A flexible serverless middleware that transforms and routes Fullstory data across multiple cloud platforms
# Clone the repository
git clone [repository-url]
# Install dependencies
npm install
# Set environment variables (.env file or export)
export CLOUD_PROVIDER=GCP # Options: GCP, AZURE, AWS
export NODE_ENV=development
# Run locally
npm start
# Run in Docker
npm run docker:build
npm run docker:run:env
- Overview
- Features
- Architecture
- Design Patterns
- Service Registry
- Startup Sequence
- Service Connectors
- Configuration
- Deployment
- Local Development
- Testing
- Contributing
Lexicon is a multi-cloud serverless middleware that processes Fullstory data and routes it to various destinations. It serves as an intermediary layer between Fullstory analytics and downstream services, transforming and enriching data along the way.
Key Benefits:
- Single codebase deployable to GCP, Azure, or AWS
- Automatic cloud provider detection
- Flexible webhook routing system
- Comprehensive connector ecosystem for third-party integrations
- Multi-Cloud Deployment: Deploy the same code to Google Cloud, Azure, or AWS
- Webhook Processing: Handle Fullstory webhook events and route them appropriately
- Service Integrations: Connect with Slack, Jira, BigQuery, Snowflake, and more
- Database Abstraction: Work with multiple database backends through a common interface
- Robust Configuration: Type-safe configuration with environment-specific settings
- Service Registry: Centralized service management with dependency injection
- Controlled Startup Sequence: Phased initialization to prevent circular dependencies
- Comprehensive Logging: Structured logging with redaction of sensitive information
- Docker Support: Run in containers locally or in production environments
Lexicon uses an adapter pattern to deploy the same application to multiple cloud environments:
βββββββββββββββββββββββββββββββββββββββ
β Fullstory Anywhere Activations β
ββββββββββββββββββββ¬βββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββ
β Configure Webhook Properties β
β (Event Types, User Attributes) β
ββββββββββββββββββββ¬βββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββ
β Fullstory Webhook β
ββββββββββββββββββββ¬βββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββ
β Webhook Verification β
β (Signature & Auth Check) β
ββββββββββββββββββββ¬βββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββ
β Cloud Adapter (GCP/AWS/Azure) β
ββββββββββββββββββββ¬βββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββ
β Service Registry Layer β
β (Dependency Injection & Sharing) β
ββββββββββββββββββββ¬βββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββ
β Data Transformation & Enrichment β
β (Using Connectors for Context) β
ββββββββββββββββββββ¬βββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββ
β Connector Information Fetch β
β (Fullstory, BigQuery, etc.) β
ββββββββββββββββββββ¬βββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββ
β Route Configuration β
β (Destination & Format Rules) β
ββββββββββββββββββββ¬βββββββββββββββββββ
β
β βββ One behavioral webhook can feed
β many end destinations at once
ββββββββββββββββββββΌβββββββββββββββββββ
β Connector Services β
βββββββ¬ββββββββββ¬ββββββββββ¬ββββββββββββ
β β β
β β β
βββββββΌββββ βββββΌββββ βββββΌβββββββββ
βDatabasesβ β APIs β β Endpoints β
βββββββββββ βββββββββ ββββββββββββββ
Lexicon follows several best practices and design patterns:
-
Adapter Pattern
- Abstracts cloud-specific implementations
- Enables a single codebase across platforms
-
Singleton Pattern
- Used for service connectors to maintain consistent state
- Example:
const fullstoryClient = new FullstoryClient(token, orgId);
-
Factory Pattern
- Creates appropriate cloud adapters at runtime
- Example:
function createCloudAdapter(provider) { switch (provider.toUpperCase()) { case 'GCP': return new GCPAdapter(); case 'AZURE': return new AzureAdapter(); case 'AWS': return new AWSAdapter(); } }
-
Builder Pattern
- Used in database query construction
- Example:
konbini.warehouse.generateSql({ ... })
-
Strategy Pattern
- Implemented for different authentication methods
-
Service Registry Pattern
- Centralizes and manages shared services
- Eliminates circular dependencies
- Simplifies testing through dependency injection
- Example:
// Register a service serviceRegistry.register('config', configInstance); // Get a service const config = serviceRegistry.get('config');
- DRY (Don't Repeat Yourself): Common functionality in utility methods
- Single Responsibility: Each class/function has a focused purpose
- Error Handling: Consistent error responses across endpoints
- Configuration Management: Centralized through
config.js
- Protected Methods: Private methods prefixed with underscore
The Service Registry is a core architectural component that provides centralized service management and dependency injection:
βββββββββββββββββββββββββββββββββββββββ
β serviceRegistry.js β
β (Central Service Repository) β
βββββββ¬ββββββββββ¬ββββββββββ¬ββββββββββββ
β β β
β β β
βββββββΌββββ βββββΌββββ βββββΌβββββββββ
β config β βinitialβ β connectors β
β β βizationβ β β
βββββββββββ βββββββββ ββββββββββββββ
Key Benefits:
- Dependency Management: Centralized management of service instances
- Circular Dependency Prevention: Break dependency cycles between modules
- Testing Support: Easy mocking of services during unit tests
- Runtime Flexibility: Services can be dynamically registered and replaced
Usage Examples:
// In index.js during application startup
serviceRegistry.register('config', configInstance);
serviceRegistry.register('initialization', initializationInstance);
// In a connector or webhook handler
const config = serviceRegistry.get('config');
const initialization = serviceRegistry.get('initialization');
// Check if a service exists
if (serviceRegistry.has('snowflake')) {
const snowflake = serviceRegistry.get('snowflake');
// Use the snowflake connector
}
Service Registry API:
// Register a service
serviceRegistry.register('serviceName', serviceInstance);
// Get a registered service
const service = serviceRegistry.get('serviceName');
// Check if a service exists
const exists = serviceRegistry.has('serviceName');
// Get all registered service names
const services = serviceRegistry.getServiceNames();
Lexicon implements a controlled, phased initialization process that prevents circular dependencies and ensures services are initialized in the correct order:
βββββββββββββββββββββββββββββββββββββββ
β startup.js β
β (Initialization Manager) β
βββββββββββββββββββ¬ββββββββββββββββββ¬ββ
β β
βΌ βΌ
βββββββββββββββββββββββββββ βββββββββββββββββββββββββ
β Initialization β β Service Registry β
β Phases β β Registration β
ββββββββββββ¬βββββββββββββββ βββββββββββββ¬ββββββββββββ
β β
βΌ βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Phase 1: Core Services β
β config, initialization, middleware β
ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Phase 2: Database & Resources β
β konbini, snowflake, bigQuery, workspace β
ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Phase 3: External Integrations β
β fullstory, slack, atlassian β
ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Phase 4: Webhooks & Routes β
β webhookRouter β
ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Phase 5: Cloud Adapter β
β cloudAdapter β
ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Initialization Summary β
β Status reporting for all components β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Phased Initialization: Services are started in a specific order to prevent dependency issues
- Graceful Fallbacks: Services can operate even when dependencies aren't fully initialized
- Robust Error Handling: Initialization failures in one service don't crash the entire application
- Status Reporting: Comprehensive logging of initialization state for all components
- Core Services: Essential services like config and the service registry itself are initialized first
- Database & Resources: Data storage and resource connectors are initialized next
- External Integrations: Third-party service connectors like Fullstory, Slack, and Atlassian
- Webhooks & Routes: API endpoints and webhook handlers are set up
- Cloud Adapter: The cloud-specific adapter (GCP, AWS, Azure) is initialized last
// In index.js
const startup = require('./startup');
// Initialize all services in the correct sequence
await startup.initialize();
// Get initialization status
const status = startup.getStatus();
console.log(`Initialization complete: ${status.initialized}`);
console.log(`Services registered: ${status.serviceCount}`);
The startup sequence is designed to be resilient:
- Temporary Placeholder: The application exports a placeholder function immediately, which responds with a 503 status code during initialization
- Individual Service Failures: If a service fails to initialize, the system continues with the next service
- Graceful Degradation: The application functions with reduced capabilities when non-critical services fail
- Comprehensive Logging: Detailed error logs help identify initialization issues
Lexicon includes several service connectors to integrate with external systems:
Interact with Fullstory's behavioral data platform:
// Get session data
const summary = await Fullstory.getSessionSummary(userId, sessionId);
// Generate session replay link
const link = Fullstory.getSessionLink(userId, sessionId);
Work with multiple database backends through Konbini:
// BigQuery example with named parameters
const { sql, params, parameterTypes } = konbini.warehouse.generateSql({
databaseType: 'bigquery',
operation: 'insert',
table: 'fs_data_destinations.lead_info',
columns: ['session_id', 'visitor_id'],
data: {
session_id: data.session_id,
visitor_id: data.uid
}
});
// Snowflake example with positional parameters
await snowflake.withConnection(async (connector) => {
await connector.executeQuery(sql, bindings);
});
Send notifications to various channels:
// Send Slack notification
await slack.sendWebHook({
text: "New customer feedback received",
blocks: [/* Block Kit content */]
});
// Create Jira ticket
const ticket = await atlassian.jira.createTicket({
fields: {
summary: `${body.name} - ${body.user.email}`,
description: rundown,
project: { key: projectKey },
issuetype: { id: issueTypeId }
}
});
Lexicon provides a robust configuration system through config.js
:
// Get a configuration value with default
const port = config.get('port', 8080);
// Get a typed configuration value
const debugEnabled = config.getBoolean('enable_detailed_errors', false);
const timeout = config.getNumber('timeout', 30);
// Check environment
if (config.isCloudProvider('GCP')) {
// GCP-specific code
}
Configuration can be set through:
- Environment variables
.env
file (development only)- Cloud provider environment settings
Lexicon follows a layered configuration architecture to ensure consistency and validation across all services:
βββββββββββββββββββββββββββββββ
β config.js β
β (Core Configuration System) β
βββββββββββββββ¬ββββββββββββββββ
β
β provides configuration
βΌ
βββββββββββββββββββββββββββββββ
β connectorConfigValidator β
β (Validation & Typing) β
βββββββββββββββ¬ββββββββββββββββ
β
β provides validation services
βΌ
βββββββββββββββββββββββββββββββ
β connectorBase.js β
β (Common Connector Logic) β
βββββββββββββββ¬ββββββββββββββββ
β
β extends
ββββββββββΌβββββββββ¬ββββββββββββ
β β β β
βΌ βΌ βΌ βΌ
βββββββββββ βββββββ ββββββββββ ββββββββββ
βSnowflakeβ βSlackβ βFullstoryβ β ... β
βββββββββββ βββββββ ββββββββββ ββββββββββ
Key Components:
-
config.js: Singleton configuration manager that handles environment detection, environment variables, and cloud platform specifics.
-
connectorConfigValidator.js: Validates configuration values, tracks errors, and provides proper type conversion.
-
connectorBase.js: Provides a consistent interface to all connectors, integrating the validator with convenient helper methods.
-
Individual Connectors: Extend ConnectorBase to inherit the configuration system.
All service connectors use a consistent pattern to access configuration:
// Creating a new connector that extends ConnectorBase
class SnowflakeConnector extends ConnectorBase {
constructor() {
// Initialize with connector name
super('Snowflake');
// Get configuration through the base class methods
this.config = {
account: this.getConfig('snowflake_account_identifier'),
username: this.getConfig('snowflake_user'),
warehouse: this.getConfig('snowflake_warehouse'),
// Get more configuration values as needed
};
// Check if configuration is valid using the validator
this.isConfigured = this.validator.checkIsConfigured();
}
// Access configuration in methods
async connect() {
if (!this.isConfigured) {
return Promise.reject(new Error('Snowflake is not properly configured'));
}
// Use configuration values
// ...
}
}
This approach ensures:
- Consistent configuration across all connectors
- Proper validation and error tracking
- Type-safe configuration access
- Environment-specific configuration
# Deploy to Cloud Functions
gcloud functions deploy lexicon \
--runtime=nodejs18 \
--trigger-http \
--set-env-vars="cloud_provider=GCP"
# Deploy to Cloud Run
gcloud run deploy lexicon \
--image=gcr.io/[PROJECT_ID]/lexicon \
--set-env-vars="cloud_provider=GCP"
# Deploy to Azure Functions
func azure functionapp publish [APP_NAME] \
--javascript \
--set cloud_provider=AZURE
# Deploy to Azure App Service
az webapp deploy \
--resource-group [RESOURCE_GROUP] \
--name [APP_NAME] \
--src-path . \
--type zip \
--env-vars cloud_provider=AZURE
# Deploy to AWS App Runner
npm run deploy:aws
# (Requires AWS_ACCOUNT_ID and AWS_REGION env variables)
Lexicon provides several Docker-related npm scripts:
# Build and run
npm run docker:build
npm run docker:run:env # Uses env vars from .env file
# Provider-specific containers
npm run docker:run:gcp # Runs on port 8080
npm run docker:run:azure # Runs on port 8080
npm run docker:run:aws # Runs on port 8080
For local development:
# Set cloud provider
export CLOUD_PROVIDER=GCP
# Run with local environment
npm start
# Run with Docker and live reload
npm run dev:mount
# Auto-restart on changes
npm run dev:docker
Lexicon includes comprehensive tests:
# Run all tests
npm test
# Run specific test categories
npm run test:unit
npm run test:integration
npm run test:adapters
# Test specific cloud providers
npm run test:gcp
npm run test:azure
npm run test:aws
# Watch mode for development
npm run test:watch
For detailed test documentation, see the Testing Guidelines.
When adding new functionality to Lexicon, follow these guidelines:
-
New Webhook Handlers:
- Extend
WebhookBase
class - Use the logger and errorHandler
- Follow existing patterns
- Extend
-
New Connector Integrations:
- Create a dedicated file that extends
ConnectorBase
- Use the configuration validation system through
getConfig()
methods - Implement comprehensive error handling
- Document with JSDoc comments
- Create a dedicated file that extends
-
Cloud Provider Support:
- Extend the appropriate adapter
- Test thoroughly in target environment
This project is licensed under the ISC License - see the LICENSE file for details.