string;
experiment_id?: string;
referrer?: string;
device_type: 'mobile' | 'desktop' | 'tablet';
};
metrics: {
time_to_interact_ms?: number;
success: boolean;
error_code?: string;
};
}
export const FEATURE_EVENTS = {
VIEW: 'feature_view',
INTERACT: 'feature_interact',
CONVERT: 'feature_convert',
ERROR: 'feature_error',
} as const;
### Step 2: Feature Flag Wrapper with Telemetry
Feature flags are the control mechanism for discovery. However, flags must be instrumented to capture exposure and interaction automatically. Avoid scattering tracking code; instead, create a wrapper that handles evaluation and telemetry side-effects.
```typescript
// services/feature-discovery-service.ts
import { FEATURE_EVENTS, FeatureDiscoveryEvent } from './schema/discovery-events';
export class FeatureDiscoveryService {
private analyticsClient: AnalyticsClient;
private flagProvider: FlagProvider;
constructor(analytics: AnalyticsClient, flags: FlagProvider) {
this.analyticsClient = analytics;
this.flagProvider = flags;
}
/**
* Evaluates a feature flag and tracks exposure.
* Returns the variant and automatically logs the view event.
*/
async evaluateAndTrack(
featureId: string,
context: Partial<FeatureDiscoveryEvent['context']>
): Promise<{ enabled: boolean; variant: string }> {
const evaluation = await this.flagProvider.evaluate(featureId);
// Track exposure immediately upon evaluation
this.track({
event_name: FEATURE_EVENTS.VIEW,
feature_id: featureId,
context: {
...context,
variant: evaluation.variant,
experiment_id: evaluation.experimentId,
},
metrics: { success: true },
});
return {
enabled: evaluation.enabled,
variant: evaluation.variant,
};
}
/**
* Tracks specific user interactions with a feature.
*/
trackInteraction(
featureId: string,
interactionType: 'click' | 'submit' | 'hover',
duration?: number
) {
this.track({
event_name: FEATURE_EVENTS.INTERACT,
feature_id: featureId,
context: { interaction_type: interactionType },
metrics: {
success: true,
time_to_interact_ms: duration,
},
});
}
/**
* Tracks errors within feature boundaries.
*/
trackError(featureId: string, error: Error) {
this.track({
event_name: FEATURE_EVENTS.ERROR,
feature_id: featureId,
metrics: {
success: false,
error_code: error.message,
},
});
}
private track(event: Omit<FeatureDiscoveryEvent, 'user_id' | 'session_id' | 'timestamp'>) {
const enrichedEvent: FeatureDiscoveryEvent = {
...event,
timestamp: Date.now(),
user_id: this.getUserId(),
session_id: this.getSessionId(),
};
// Fire-and-forget with retry logic for production reliability
this.analyticsClient.capture(enrichedEvent).catch(console.error);
}
private getUserId(): string { /* Implementation */ return ''; }
private getSessionId(): string { /* Implementation */ return ''; }
}
Step 3: Architecture Decisions and Rationale
Decision: Client-Side vs. Server-Side Evaluation
- Implementation: Use server-side evaluation for core feature logic to ensure consistency and security, but mirror exposure events to the client for UI interaction tracking.
- Rationale: Server-side evaluation prevents flag state leakage and ensures accurate A/B testing. Client-side tracking captures granular UI interactions (clicks, hovers) that servers cannot observe.
Decision: Batched vs. Real-Time Telemetry
- Implementation: Buffer events in the client SDK and flush in batches (e.g., every 2 seconds or 10 events).
- Rationale: Real-time HTTP requests for every interaction degrade performance and increase payload overhead. Batching reduces network requests by ~90% while maintaining sufficient freshness for discovery dashboards.
Decision: Schema Governance
- Implementation: Enforce event schemas via a shared TypeScript package across frontend and backend services.
- Rationale: Decoupled teams often introduce breaking changes to event payloads. A shared package ensures type safety and prevents data pipeline failures caused by schema drift.
Step 4: Discovery Dashboard Integration
The data pipeline should feed a discovery dashboard that correlates feature exposure with business outcomes. Key queries should include:
- Adoption Funnel: Exposure β Interaction β Conversion.
- Stickiness: Retention of users who interacted with the feature vs. those who did not.
- Error Rate: Feature-specific error rates compared to baseline.
Pitfall Guide
1. Event Sprawl and Schema Drift
Mistake: Adding events ad-hoc without a central registry.
Impact: The analytics warehouse becomes unqueryable. Teams cannot trust the data, leading to decision paralysis.
Best Practice: Maintain a events.json or TypeScript enum file as the source of truth. CI pipelines should validate that all tracked events exist in the schema.
2. Flag Rot
Mistake: Leaving feature flags in the codebase after a feature is fully rolled out.
Impact: Increased code complexity, dead code paths, and performance degradation due to unnecessary evaluations.
Best Practice: Implement a "Flag Lifecycle Policy." Flags older than 30 days with 100% rollout trigger automated PRs to remove the flag code.
3. Vanity Metrics Over Actionable Signals
Mistake: Tracking feature_view but ignoring feature_error or time_to_interact.
Impact: Teams assume a feature is successful based on views, missing usability issues or performance bottlenecks.
Best Practice: Always pair exposure metrics with success metrics. A high view count with low interaction indicates a discovery or UX failure.
4. Correlation vs. Causation Errors
Mistake: Attributing churn to a feature because churn spiked after deployment.
Impact: Rolling back valuable features due to confounding variables (e.g., a server outage or marketing campaign).
Best Practice: Use randomized controlled trials (A/B tests) for discovery. Compare feature users against a holdout group to isolate the feature's impact.
5. Siloed Discovery Data
Mistake: Product managers view discovery data in a BI tool, while engineers work in Jira.
Impact: Engineers lack context for why features are deprecated or prioritized.
Best Practice: Integrate discovery metrics into the engineering workflow. For example, a Slack bot that alerts the team when a feature's adoption drops below a threshold.
6. Ignoring Negative Signals
Mistake: Focusing only on happy paths.
Impact: Features that cause errors or frustration are scaled, damaging user trust.
Best Practice: Implement error tracking within feature boundaries. If a feature has an error rate >2x the baseline, automatically disable it via the flag provider.
7. Over-Instrumentation
Mistake: Tracking every micro-interaction.
Impact: High data costs and noise that obscures key signals.
Best Practice: Define a "North Star" metric for each feature. Track only events that directly contribute to calculating that metric.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Early-Stage MVP | Lightweight SDK + Manual Dashboard Review | Speed of implementation is critical; low data volume allows manual analysis. | Low |
| Enterprise Scale | CDP Integration + Automated Experimentation | Requires governance, privacy compliance, and statistical rigor for large user bases. | High |
| Regulated Industry | On-Prem Pipeline + Strict Schema Governance | Data residency requirements and audit trails necessitate controlled infrastructure. | Medium |
| Performance-Constrained | Batched Telemetry + Sampling | Reduces network overhead and client-side processing to maintain UX. | Low |
Configuration Template
Use this configuration to initialize the discovery service with sensible defaults for production environments.
// config/discovery-config.ts
import { DiscoveryConfig } from '@codcompass/discovery-sdk';
export const discoveryConfig: DiscoveryConfig = {
apiKey: process.env.DISCOVERY_API_KEY,
environment: process.env.NODE_ENV,
// Batching settings to optimize performance
batch: {
maxSize: 10,
flushIntervalMs: 2000,
retryAttempts: 3,
},
// Sampling to manage data volume
sampling: {
enabled: true,
rate: 0.5, // Sample 50% of events in non-critical environments
},
// Schema validation
validation: {
strictMode: process.env.NODE_ENV === 'production',
onError: 'log', // 'log' | 'throw' | 'drop'
},
// Feature flag integration
flags: {
provider: 'launchdarkly', // or 'statsig', 'custom'
cacheTtlSeconds: 60,
},
// Privacy controls
privacy: {
anonymizeIp: true,
maskFields: ['email', 'phone', 'ssn'],
consentRequired: true,
},
};
Quick Start Guide
-
Install the SDK:
npm install @codcompass/discovery-sdk
-
Initialize the Service:
import { FeatureDiscoveryService } from '@codcompass/discovery-sdk';
import { discoveryConfig } from './config/discovery-config';
const discovery = new FeatureDiscoveryService(discoveryConfig);
-
Wrap Feature Logic:
// In your component or controller
const { enabled, variant } = await discovery.evaluateAndTrack('new-checkout-flow', {
referrer: 'homepage',
});
if (enabled) {
// Render new feature
discovery.trackInteraction('new-checkout-flow', 'view');
}
-
Verify in Dashboard:
Navigate to your discovery dashboard. Within 2 minutes, you should see exposure events for new-checkout-flow segmented by variant.
-
Iterate:
Use the dashboard data to decide whether to roll out the feature, tweak the UX, or rollback based on adoption and error metrics.