Conversion Tracking: Goals, Funnels & Attribution Models
Conversion tracking is the cornerstone of ChatGPT app success measurement. While basic analytics tell you how many users interact with your app, conversion tracking reveals the critical moments where users complete valuable actions—from initial engagement to revenue generation. This comprehensive guide explores how to implement sophisticated conversion tracking systems that measure goals, visualize funnels, attribute conversions accurately, and optimize conversion rates.
For ChatGPT apps operating in the OpenAI ecosystem, conversion tracking presents unique challenges: users may interact across multiple sessions, conversion paths span chat conversations and widget interactions, and attribution requires understanding both model-initiated and user-initiated actions. By implementing robust conversion tracking with proper goal configuration, funnel analysis, and multi-touch attribution modeling, you transform raw interaction data into actionable insights that drive growth, improve user experience, and maximize revenue per user.
This article provides production-ready code examples for implementing comprehensive conversion tracking in your ChatGPT app, complete with goal trackers, funnel analyzers, attribution models, CRO experiment frameworks, and journey mapping systems that work seamlessly with MCP servers and ChatGPT's widget runtime.
Goal Configuration
Goal configuration defines the success metrics that matter for your ChatGPT app. Unlike traditional web analytics where goals are often page-based, ChatGPT app goals must account for conversational interactions, widget engagements, and cross-session user journeys.
Goal Types for ChatGPT Apps:
- Completion Goals: User completes a specific action (e.g., booking appointment, purchasing product)
- Engagement Goals: User reaches engagement threshold (e.g., 5+ tool calls, 10+ messages)
- Revenue Goals: Transaction with monetary value (e.g., subscription purchase, in-app payment)
- Micro-Conversion Goals: Smaller actions indicating progress (e.g., profile completion, widget interaction)
Value Assignment: Each goal should have an assigned value representing its business impact. For e-commerce goals, this is the transaction amount. For lead generation goals, assign the average customer lifetime value. For engagement goals, calculate the correlation between engagement and revenue to derive a proxy value.
Goal Verification: Implement verification logic to prevent duplicate goal tracking, validate goal completion criteria, and handle edge cases like partial conversions or user abandonments.
Production-Ready Goal Tracker Implementation
// goal-tracker.ts - Comprehensive Goal Tracking System for ChatGPT Apps
import { db } from './firestore';
import { logAnalyticsEvent } from './analytics';
export interface Goal {
id: string;
name: string;
type: 'completion' | 'engagement' | 'revenue' | 'micro_conversion';
value: number; // Monetary value or proxy value
conditions: GoalCondition[];
cooldown?: number; // Minimum time between duplicate conversions (ms)
metadata?: Record<string, any>;
}
export interface GoalCondition {
type: 'tool_call' | 'widget_interaction' | 'property' | 'session_duration' | 'custom';
operator: 'equals' | 'contains' | 'greater_than' | 'less_than' | 'exists';
field?: string;
value?: any;
count?: number; // For counting events
}
export interface ConversionEvent {
goalId: string;
userId: string;
sessionId: string;
timestamp: number;
value: number;
attributionData: {
source?: string;
medium?: string;
campaign?: string;
referrer?: string;
touchpoints: AttributionTouchpoint[];
};
metadata?: Record<string, any>;
}
export interface AttributionTouchpoint {
type: 'tool_call' | 'widget_click' | 'message' | 'external_referral';
timestamp: number;
data: Record<string, any>;
}
class GoalTracker {
private goals: Map<string, Goal> = new Map();
private userConversions: Map<string, Set<string>> = new Map(); // userId -> Set<goalId>
private lastConversionTime: Map<string, number> = new Map(); // userId:goalId -> timestamp
private sessionTouchpoints: Map<string, AttributionTouchpoint[]> = new Map(); // sessionId -> touchpoints
async loadGoals(): Promise<void> {
const goalsSnapshot = await db.collection('goals').get();
goalsSnapshot.forEach(doc => {
const goal = { id: doc.id, ...doc.data() } as Goal;
this.goals.set(goal.id, goal);
});
}
// Track a touchpoint in the user's journey
trackTouchpoint(sessionId: string, touchpoint: AttributionTouchpoint): void {
if (!this.sessionTouchpoints.has(sessionId)) {
this.sessionTouchpoints.set(sessionId, []);
}
this.sessionTouchpoints.get(sessionId)!.push(touchpoint);
// Trim to last 100 touchpoints to prevent memory bloat
const touchpoints = this.sessionTouchpoints.get(sessionId)!;
if (touchpoints.length > 100) {
touchpoints.shift();
}
}
// Check if conditions for a goal are met
private async checkGoalConditions(
goal: Goal,
context: {
userId: string;
sessionId: string;
toolCalls?: string[];
widgetInteractions?: any[];
userProperties?: Record<string, any>;
sessionDuration?: number;
}
): Promise<boolean> {
for (const condition of goal.conditions) {
switch (condition.type) {
case 'tool_call':
if (!context.toolCalls || context.toolCalls.length === 0) return false;
const toolCallCount = context.toolCalls.filter(call =>
condition.value ? call === condition.value : true
).length;
if (condition.count && toolCallCount < condition.count) return false;
break;
case 'widget_interaction':
if (!context.widgetInteractions || context.widgetInteractions.length === 0) return false;
break;
case 'property':
if (!context.userProperties || !condition.field) return false;
const propValue = context.userProperties[condition.field];
if (!this.evaluateCondition(propValue, condition.operator, condition.value)) {
return false;
}
break;
case 'session_duration':
if (!context.sessionDuration) return false;
if (!this.evaluateCondition(context.sessionDuration, condition.operator, condition.value)) {
return false;
}
break;
case 'custom':
// Custom condition logic can be implemented here
break;
}
}
return true;
}
private evaluateCondition(actualValue: any, operator: string, expectedValue: any): boolean {
switch (operator) {
case 'equals':
return actualValue === expectedValue;
case 'contains':
return String(actualValue).includes(String(expectedValue));
case 'greater_than':
return Number(actualValue) > Number(expectedValue);
case 'less_than':
return Number(actualValue) < Number(expectedValue);
case 'exists':
return actualValue !== undefined && actualValue !== null;
default:
return false;
}
}
// Track a conversion
async trackConversion(
goalId: string,
userId: string,
sessionId: string,
context: any,
attributionSource?: {
source?: string;
medium?: string;
campaign?: string;
referrer?: string;
}
): Promise<ConversionEvent | null> {
const goal = this.goals.get(goalId);
if (!goal) {
console.error(`Goal ${goalId} not found`);
return null;
}
// Check conditions
const conditionsMet = await this.checkGoalConditions(goal, { userId, sessionId, ...context });
if (!conditionsMet) {
return null;
}
// Check cooldown
const cooldownKey = `${userId}:${goalId}`;
const lastConversion = this.lastConversionTime.get(cooldownKey);
if (goal.cooldown && lastConversion) {
const timeSinceLastConversion = Date.now() - lastConversion;
if (timeSinceLastConversion < goal.cooldown) {
console.log(`Goal ${goalId} still in cooldown for user ${userId}`);
return null;
}
}
// Create conversion event
const conversionEvent: ConversionEvent = {
goalId,
userId,
sessionId,
timestamp: Date.now(),
value: goal.value,
attributionData: {
...attributionSource,
touchpoints: this.sessionTouchpoints.get(sessionId) || []
},
metadata: context.metadata || {}
};
// Save to Firestore
await db.collection('conversions').add(conversionEvent);
// Update tracking state
if (!this.userConversions.has(userId)) {
this.userConversions.set(userId, new Set());
}
this.userConversions.get(userId)!.add(goalId);
this.lastConversionTime.set(cooldownKey, conversionEvent.timestamp);
// Log analytics event
await logAnalyticsEvent('conversion', {
goal_name: goal.name,
goal_type: goal.type,
value: goal.value,
user_id: userId,
session_id: sessionId
});
return conversionEvent;
}
// Get conversion rate for a goal
async getConversionRate(goalId: string, startDate: Date, endDate: Date): Promise<number> {
const totalSessions = await db.collection('sessions')
.where('timestamp', '>=', startDate)
.where('timestamp', '<=', endDate)
.count()
.get();
const conversions = await db.collection('conversions')
.where('goalId', '==', goalId)
.where('timestamp', '>=', startDate.getTime())
.where('timestamp', '<=', endDate.getTime())
.count()
.get();
return conversions.data().count / totalSessions.data().count;
}
}
export const goalTracker = new GoalTracker();
Funnel Visualization
Funnel visualization reveals where users drop off in multi-step conversion processes. For ChatGPT apps, funnels might track the journey from initial message to tool call to widget interaction to final conversion.
Step Definition: Each funnel step represents a meaningful user action. Define steps that reflect your app's user flow: discovery → engagement → interaction → conversion.
Drop-off Analysis: Calculate the percentage of users who complete each step and identify where the largest drop-offs occur. High drop-off rates indicate friction points that need optimization.
Time Between Steps: Measure how long users take to progress between funnel steps. Extended delays may indicate confusion, technical issues, or user hesitation.
Production-Ready Funnel Analyzer
// funnel-analyzer.ts - Advanced Funnel Analysis for ChatGPT Apps
import { db } from './firestore';
export interface FunnelStep {
id: string;
name: string;
order: number;
eventType: string; // Event that triggers this step
conditions?: Record<string, any>; // Optional conditions for step completion
}
export interface Funnel {
id: string;
name: string;
steps: FunnelStep[];
dateRange?: { start: Date; end: Date };
}
export interface FunnelAnalysisResult {
funnelId: string;
funnelName: string;
totalUsers: number;
stepResults: StepResult[];
averageTimeToConvert: number; // milliseconds
conversionRate: number; // percentage
}
export interface StepResult {
stepId: string;
stepName: string;
usersEntered: number;
usersCompleted: number;
completionRate: number; // percentage
dropOffRate: number; // percentage
averageTimeToNextStep: number; // milliseconds
medianTimeToNextStep: number; // milliseconds
}
interface UserJourney {
userId: string;
stepCompletions: Map<string, number>; // stepId -> timestamp
}
class FunnelAnalyzer {
async analyzeFunnel(funnel: Funnel): Promise<FunnelAnalysisResult> {
const userJourneys = await this.collectUserJourneys(funnel);
const stepResults = this.calculateStepMetrics(funnel, userJourneys);
const usersWhoCompleted = userJourneys.filter(journey =>
this.didCompleteAllSteps(journey, funnel.steps)
).length;
const conversionTimes = userJourneys
.filter(journey => this.didCompleteAllSteps(journey, funnel.steps))
.map(journey => this.calculateTimeToConvert(journey, funnel.steps))
.filter(time => time !== null) as number[];
const averageTimeToConvert = conversionTimes.length > 0
? conversionTimes.reduce((a, b) => a + b, 0) / conversionTimes.length
: 0;
return {
funnelId: funnel.id,
funnelName: funnel.name,
totalUsers: userJourneys.length,
stepResults,
averageTimeToConvert,
conversionRate: (usersWhoCompleted / userJourneys.length) * 100
};
}
private async collectUserJourneys(funnel: Funnel): Promise<UserJourney[]> {
const journeyMap = new Map<string, UserJourney>();
// Collect events for all funnel steps
for (const step of funnel.steps) {
const query = db.collection('analytics_events')
.where('event_type', '==', step.eventType);
if (funnel.dateRange) {
query.where('timestamp', '>=', funnel.dateRange.start)
.where('timestamp', '<=', funnel.dateRange.end);
}
const events = await query.get();
events.forEach(doc => {
const event = doc.data();
const userId = event.user_id;
if (!journeyMap.has(userId)) {
journeyMap.set(userId, {
userId,
stepCompletions: new Map()
});
}
const journey = journeyMap.get(userId)!;
// Only record the first completion of each step
if (!journey.stepCompletions.has(step.id)) {
journey.stepCompletions.set(step.id, event.timestamp);
}
});
}
return Array.from(journeyMap.values());
}
private calculateStepMetrics(funnel: Funnel, userJourneys: UserJourney[]): StepResult[] {
return funnel.steps.map((step, index) => {
const usersWhoCompletedPreviousStep = index === 0
? userJourneys.length
: userJourneys.filter(journey => journey.stepCompletions.has(funnel.steps[index - 1].id)).length;
const usersWhoCompletedThisStep = userJourneys.filter(journey =>
journey.stepCompletions.has(step.id)
).length;
const timesToNextStep = this.calculateTimesToNextStep(
userJourneys,
step.id,
funnel.steps[index + 1]?.id
);
return {
stepId: step.id,
stepName: step.name,
usersEntered: usersWhoCompletedPreviousStep,
usersCompleted: usersWhoCompletedThisStep,
completionRate: (usersWhoCompletedThisStep / usersWhoCompletedPreviousStep) * 100,
dropOffRate: ((usersWhoCompletedPreviousStep - usersWhoCompletedThisStep) / usersWhoCompletedPreviousStep) * 100,
averageTimeToNextStep: this.average(timesToNextStep),
medianTimeToNextStep: this.median(timesToNextStep)
};
});
}
private calculateTimesToNextStep(
userJourneys: UserJourney[],
currentStepId: string,
nextStepId?: string
): number[] {
if (!nextStepId) return [];
return userJourneys
.filter(journey =>
journey.stepCompletions.has(currentStepId) &&
journey.stepCompletions.has(nextStepId)
)
.map(journey => {
const currentTime = journey.stepCompletions.get(currentStepId)!;
const nextTime = journey.stepCompletions.get(nextStepId)!;
return nextTime - currentTime;
});
}
private didCompleteAllSteps(journey: UserJourney, steps: FunnelStep[]): boolean {
return steps.every(step => journey.stepCompletions.has(step.id));
}
private calculateTimeToConvert(journey: UserJourney, steps: FunnelStep[]): number | null {
if (!this.didCompleteAllSteps(journey, steps)) return null;
const firstStepTime = journey.stepCompletions.get(steps[0].id)!;
const lastStepTime = journey.stepCompletions.get(steps[steps.length - 1].id)!;
return lastStepTime - firstStepTime;
}
private average(numbers: number[]): number {
if (numbers.length === 0) return 0;
return numbers.reduce((a, b) => a + b, 0) / numbers.length;
}
private median(numbers: number[]): number {
if (numbers.length === 0) return 0;
const sorted = [...numbers].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0
? (sorted[mid - 1] + sorted[mid]) / 2
: sorted[mid];
}
}
export const funnelAnalyzer = new FunnelAnalyzer();
Attribution Models
Attribution modeling determines which touchpoints deserve credit for conversions. ChatGPT apps require sophisticated attribution because users may interact through multiple channels (direct chat, widget clicks, external referrals) before converting.
Last-Click Attribution: Gives 100% credit to the final touchpoint before conversion. Simple but ignores the contribution of earlier interactions.
First-Click Attribution: Credits the first touchpoint that introduced the user to your app. Useful for measuring top-of-funnel effectiveness.
Linear Attribution: Distributes credit equally across all touchpoints. Fair but doesn't account for varying touchpoint importance.
Position-Based Attribution: Gives more weight to first and last touchpoints (e.g., 40% each) with remaining credit distributed to middle touchpoints. Balances awareness and conversion credit.
Production-Ready Attribution Modeler
// attribution-modeler.ts - Multi-Touch Attribution for ChatGPT Apps
import { ConversionEvent, AttributionTouchpoint } from './goal-tracker';
export type AttributionModel = 'last_click' | 'first_click' | 'linear' | 'position_based' | 'time_decay' | 'data_driven';
export interface AttributionCredit {
touchpointIndex: number;
touchpoint: AttributionTouchpoint;
credit: number; // 0-1 (percentage of total credit)
creditValue: number; // Monetary value based on conversion value
}
export interface AttributionReport {
model: AttributionModel;
totalConversions: number;
totalValue: number;
creditByType: Map<string, number>; // touchpoint type -> total credit value
creditByChannel: Map<string, number>; // source/medium -> total credit value
}
class AttributionModeler {
// Calculate attribution credits for a single conversion
calculateAttribution(
conversion: ConversionEvent,
model: AttributionModel = 'position_based'
): AttributionCredit[] {
const touchpoints = conversion.attributionData.touchpoints;
if (touchpoints.length === 0) {
return [];
}
switch (model) {
case 'last_click':
return this.lastClickAttribution(touchpoints, conversion.value);
case 'first_click':
return this.firstClickAttribution(touchpoints, conversion.value);
case 'linear':
return this.linearAttribution(touchpoints, conversion.value);
case 'position_based':
return this.positionBasedAttribution(touchpoints, conversion.value);
case 'time_decay':
return this.timeDecayAttribution(touchpoints, conversion.value);
default:
return this.linearAttribution(touchpoints, conversion.value);
}
}
private lastClickAttribution(touchpoints: AttributionTouchpoint[], value: number): AttributionCredit[] {
const lastIndex = touchpoints.length - 1;
return touchpoints.map((touchpoint, index) => ({
touchpointIndex: index,
touchpoint,
credit: index === lastIndex ? 1.0 : 0.0,
creditValue: index === lastIndex ? value : 0
}));
}
private firstClickAttribution(touchpoints: AttributionTouchpoint[], value: number): AttributionCredit[] {
return touchpoints.map((touchpoint, index) => ({
touchpointIndex: index,
touchpoint,
credit: index === 0 ? 1.0 : 0.0,
creditValue: index === 0 ? value : 0
}));
}
private linearAttribution(touchpoints: AttributionTouchpoint[], value: number): AttributionCredit[] {
const creditPerTouchpoint = 1.0 / touchpoints.length;
const valuePerTouchpoint = value / touchpoints.length;
return touchpoints.map((touchpoint, index) => ({
touchpointIndex: index,
touchpoint,
credit: creditPerTouchpoint,
creditValue: valuePerTouchpoint
}));
}
private positionBasedAttribution(touchpoints: AttributionTouchpoint[], value: number): AttributionCredit[] {
if (touchpoints.length === 1) {
return [{
touchpointIndex: 0,
touchpoint: touchpoints[0],
credit: 1.0,
creditValue: value
}];
}
const firstCredit = 0.4;
const lastCredit = 0.4;
const middleCredit = 0.2;
const middleTouchpoints = touchpoints.length - 2;
const creditPerMiddle = middleTouchpoints > 0 ? middleCredit / middleTouchpoints : 0;
return touchpoints.map((touchpoint, index) => {
let credit: number;
if (index === 0) {
credit = firstCredit;
} else if (index === touchpoints.length - 1) {
credit = lastCredit;
} else {
credit = creditPerMiddle;
}
return {
touchpointIndex: index,
touchpoint,
credit,
creditValue: credit * value
};
});
}
private timeDecayAttribution(touchpoints: AttributionTouchpoint[], value: number): AttributionCredit[] {
// Half-life of 7 days (604800000 ms)
const halfLife = 7 * 24 * 60 * 60 * 1000;
const conversionTime = touchpoints[touchpoints.length - 1].timestamp;
// Calculate decay weights
const weights = touchpoints.map(touchpoint => {
const timeDiff = conversionTime - touchpoint.timestamp;
return Math.pow(0.5, timeDiff / halfLife);
});
const totalWeight = weights.reduce((a, b) => a + b, 0);
return touchpoints.map((touchpoint, index) => {
const credit = weights[index] / totalWeight;
return {
touchpointIndex: index,
touchpoint,
credit,
creditValue: credit * value
};
});
}
// Generate attribution report across all conversions
async generateReport(
conversions: ConversionEvent[],
model: AttributionModel
): Promise<AttributionReport> {
const creditByType = new Map<string, number>();
const creditByChannel = new Map<string, number>();
let totalValue = 0;
for (const conversion of conversions) {
const credits = this.calculateAttribution(conversion, model);
totalValue += conversion.value;
for (const credit of credits) {
// Aggregate by touchpoint type
const type = credit.touchpoint.type;
creditByType.set(type, (creditByType.get(type) || 0) + credit.creditValue);
// Aggregate by channel (source/medium)
const source = conversion.attributionData.source || 'direct';
const medium = conversion.attributionData.medium || 'none';
const channel = `${source}/${medium}`;
creditByChannel.set(channel, (creditByChannel.get(channel) || 0) + credit.creditValue);
}
}
return {
model,
totalConversions: conversions.length,
totalValue,
creditByType,
creditByChannel
};
}
}
export const attributionModeler = new AttributionModeler();
Conversion Rate Optimization
Conversion rate optimization (CRO) systematically improves conversion rates through experimentation. For ChatGPT apps, CRO involves testing different prompts, widget designs, tool call sequences, and messaging strategies.
A/B Testing Framework: Implement controlled experiments where users are randomly assigned to variants. Track conversion rates for each variant and determine statistical significance.
Variant Tracking: Ensure each user consistently sees the same variant across sessions. Store variant assignments in user properties or session data.
Statistical Significance: Use proper statistical tests (e.g., chi-squared test) to determine if observed differences are statistically significant rather than random variance.
Production-Ready CRO Experiment Tracker
// cro-experiment-tracker.ts - A/B Testing for ChatGPT Apps
import { db } from './firestore';
import { goalTracker } from './goal-tracker';
export interface Experiment {
id: string;
name: string;
description: string;
status: 'draft' | 'running' | 'paused' | 'completed';
startDate: Date;
endDate?: Date;
goalId: string; // Goal being optimized
variants: Variant[];
trafficAllocation: number[]; // Percentage of traffic for each variant (must sum to 100)
}
export interface Variant {
id: string;
name: string;
description: string;
changes: Record<string, any>; // Configuration changes for this variant
}
export interface ExperimentResult {
experimentId: string;
variantResults: VariantResult[];
winner?: string; // Variant ID of winning variant
confidenceLevel: number; // 0-100
recommendedAction: 'continue' | 'declare_winner' | 'stop_test';
}
export interface VariantResult {
variantId: string;
variantName: string;
impressions: number;
conversions: number;
conversionRate: number;
totalValue: number;
averageValue: number;
}
class CROExperimentTracker {
private activeExperiments: Map<string, Experiment> = new Map();
private userVariants: Map<string, Map<string, string>> = new Map(); // userId -> experimentId -> variantId
async loadActiveExperiments(): Promise<void> {
const experiments = await db.collection('experiments')
.where('status', '==', 'running')
.get();
experiments.forEach(doc => {
const experiment = { id: doc.id, ...doc.data() } as Experiment;
this.activeExperiments.set(experiment.id, experiment);
});
}
// Assign user to a variant
assignVariant(userId: string, experimentId: string): string {
// Check if user already has a variant assignment
if (this.userVariants.has(userId)) {
const userAssignments = this.userVariants.get(userId)!;
if (userAssignments.has(experimentId)) {
return userAssignments.get(experimentId)!;
}
}
const experiment = this.activeExperiments.get(experimentId);
if (!experiment) {
throw new Error(`Experiment ${experimentId} not found`);
}
// Randomly assign based on traffic allocation
const random = Math.random() * 100;
let cumulativeAllocation = 0;
let assignedVariantId = experiment.variants[0].id;
for (let i = 0; i < experiment.variants.length; i++) {
cumulativeAllocation += experiment.trafficAllocation[i];
if (random <= cumulativeAllocation) {
assignedVariantId = experiment.variants[i].id;
break;
}
}
// Store assignment
if (!this.userVariants.has(userId)) {
this.userVariants.set(userId, new Map());
}
this.userVariants.get(userId)!.set(experimentId, assignedVariantId);
// Persist to Firestore
db.collection('users').doc(userId).update({
[`experimentAssignments.${experimentId}`]: assignedVariantId
});
return assignedVariantId;
}
// Track an impression (user saw variant)
async trackImpression(userId: string, experimentId: string, variantId: string): Promise<void> {
await db.collection('experiment_impressions').add({
experimentId,
variantId,
userId,
timestamp: Date.now()
});
}
// Analyze experiment results
async analyzeExperiment(experimentId: string): Promise<ExperimentResult> {
const experiment = this.activeExperiments.get(experimentId);
if (!experiment) {
throw new Error(`Experiment ${experimentId} not found`);
}
const variantResults: VariantResult[] = [];
for (const variant of experiment.variants) {
// Count impressions
const impressions = await db.collection('experiment_impressions')
.where('experimentId', '==', experimentId)
.where('variantId', '==', variant.id)
.count()
.get();
// Get conversions
const conversions = await db.collection('conversions')
.where('goalId', '==', experiment.goalId)
.where('metadata.experimentId', '==', experimentId)
.where('metadata.variantId', '==', variant.id)
.get();
const conversionCount = conversions.size;
const totalValue = conversions.docs.reduce((sum, doc) => sum + (doc.data().value || 0), 0);
variantResults.push({
variantId: variant.id,
variantName: variant.name,
impressions: impressions.data().count,
conversions: conversionCount,
conversionRate: (conversionCount / impressions.data().count) * 100,
totalValue,
averageValue: conversionCount > 0 ? totalValue / conversionCount : 0
});
}
// Determine winner using chi-squared test
const { winner, confidenceLevel } = this.determineWinner(variantResults);
return {
experimentId,
variantResults,
winner,
confidenceLevel,
recommendedAction: confidenceLevel >= 95 ? 'declare_winner' :
confidenceLevel >= 80 ? 'continue' : 'stop_test'
};
}
private determineWinner(results: VariantResult[]): { winner?: string; confidenceLevel: number } {
if (results.length < 2) {
return { confidenceLevel: 0 };
}
// Find variant with highest conversion rate
const sortedResults = [...results].sort((a, b) => b.conversionRate - a.conversionRate);
const bestVariant = sortedResults[0];
const secondBestVariant = sortedResults[1];
// Chi-squared test for statistical significance
const chiSquared = this.chiSquaredTest(
bestVariant.conversions,
bestVariant.impressions,
secondBestVariant.conversions,
secondBestVariant.impressions
);
const confidenceLevel = this.getConfidenceLevel(chiSquared);
return {
winner: confidenceLevel >= 95 ? bestVariant.variantId : undefined,
confidenceLevel
};
}
private chiSquaredTest(
conversions1: number,
impressions1: number,
conversions2: number,
impressions2: number
): number {
const rate1 = conversions1 / impressions1;
const rate2 = conversions2 / impressions2;
const pooledRate = (conversions1 + conversions2) / (impressions1 + impressions2);
const expected1 = impressions1 * pooledRate;
const expected2 = impressions2 * pooledRate;
const chiSquared =
Math.pow(conversions1 - expected1, 2) / expected1 +
Math.pow(conversions2 - expected2, 2) / expected2;
return chiSquared;
}
private getConfidenceLevel(chiSquared: number): number {
// Simplified confidence level calculation (1 degree of freedom)
if (chiSquared >= 3.841) return 95;
if (chiSquared >= 2.706) return 90;
if (chiSquared >= 1.642) return 80;
return 50;
}
}
export const croExperimentTracker = new CROExperimentTracker();
Multi-Touch Attribution
Multi-touch attribution maps the complete customer journey from first awareness to final conversion. For ChatGPT apps, this includes external referrals, direct chat interactions, widget engagements, and cross-session touchpoints.
Customer Journey Mapping: Track every meaningful interaction across sessions, devices, and channels. Build a comprehensive view of how users discover, evaluate, and convert.
Cross-Session Tracking: Use persistent user IDs (Firebase Auth UID) to connect interactions across multiple chat sessions, ensuring you capture the full journey even when users return days or weeks later.
Production-Ready Journey Mapper
// journey-mapper.ts - Customer Journey Mapping for ChatGPT Apps
import { db } from './firestore';
import { AttributionTouchpoint } from './goal-tracker';
export interface CustomerJourney {
userId: string;
firstSeen: number;
lastSeen: number;
touchpoints: JourneyTouchpoint[];
conversions: JourneyConversion[];
journeyDuration: number; // milliseconds
totalSessions: number;
totalValue: number;
}
export interface JourneyTouchpoint extends AttributionTouchpoint {
sessionId: string;
channel?: string;
campaign?: string;
}
export interface JourneyConversion {
goalId: string;
goalName: string;
timestamp: number;
value: number;
touchpointIndex: number; // Which touchpoint led to conversion
}
class JourneyMapper {
async mapUserJourney(userId: string, startDate?: Date, endDate?: Date): Promise<CustomerJourney> {
// Collect all touchpoints
const touchpointsQuery = db.collection('user_touchpoints')
.where('userId', '==', userId)
.orderBy('timestamp', 'asc');
if (startDate) touchpointsQuery.where('timestamp', '>=', startDate.getTime());
if (endDate) touchpointsQuery.where('timestamp', '<=', endDate.getTime());
const touchpointsSnapshot = await touchpointsQuery.get();
const touchpoints: JourneyTouchpoint[] = touchpointsSnapshot.docs.map(doc => doc.data() as JourneyTouchpoint);
// Collect all conversions
const conversionsQuery = db.collection('conversions')
.where('userId', '==', userId)
.orderBy('timestamp', 'asc');
if (startDate) conversionsQuery.where('timestamp', '>=', startDate.getTime());
if (endDate) conversionsQuery.where('timestamp', '<=', endDate.getTime());
const conversionsSnapshot = await conversionsQuery.get();
const conversions: JourneyConversion[] = conversionsSnapshot.docs.map(doc => {
const data = doc.data();
return {
goalId: data.goalId,
goalName: data.goalName || 'Unknown',
timestamp: data.timestamp,
value: data.value,
touchpointIndex: this.findRelevantTouchpoint(touchpoints, data.timestamp)
};
});
const sessionIds = new Set(touchpoints.map(t => t.sessionId));
const totalValue = conversions.reduce((sum, c) => sum + c.value, 0);
return {
userId,
firstSeen: touchpoints.length > 0 ? touchpoints[0].timestamp : Date.now(),
lastSeen: touchpoints.length > 0 ? touchpoints[touchpoints.length - 1].timestamp : Date.now(),
touchpoints,
conversions,
journeyDuration: touchpoints.length > 0
? touchpoints[touchpoints.length - 1].timestamp - touchpoints[0].timestamp
: 0,
totalSessions: sessionIds.size,
totalValue
};
}
private findRelevantTouchpoint(touchpoints: JourneyTouchpoint[], conversionTime: number): number {
// Find the touchpoint immediately before conversion
for (let i = touchpoints.length - 1; i >= 0; i--) {
if (touchpoints[i].timestamp <= conversionTime) {
return i;
}
}
return 0;
}
// Visualize journey as text-based diagram
visualizeJourney(journey: CustomerJourney): string {
let output = `Customer Journey for User ${journey.userId}\n`;
output += `Duration: ${this.formatDuration(journey.journeyDuration)}\n`;
output += `Total Sessions: ${journey.totalSessions}\n`;
output += `Total Value: $${journey.totalValue.toFixed(2)}\n\n`;
journey.touchpoints.forEach((touchpoint, index) => {
const conversion = journey.conversions.find(c => c.touchpointIndex === index);
output += `[${index + 1}] ${new Date(touchpoint.timestamp).toISOString()}\n`;
output += ` Type: ${touchpoint.type}\n`;
if (touchpoint.channel) output += ` Channel: ${touchpoint.channel}\n`;
if (conversion) {
output += ` ✓ CONVERSION: ${conversion.goalName} ($${conversion.value})\n`;
}
output += '\n';
});
return output;
}
private formatDuration(ms: number): string {
const days = Math.floor(ms / (24 * 60 * 60 * 1000));
const hours = Math.floor((ms % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
return `${days} days, ${hours} hours`;
}
}
export const journeyMapper = new JourneyMapper();
Conversion Dashboard Component
Visualize conversion metrics, funnel performance, and attribution data in a comprehensive React dashboard component.
// ConversionDashboard.tsx - Conversion Analytics Dashboard
import React, { useEffect, useState } from 'react';
import { goalTracker, Goal } from './goal-tracker';
import { funnelAnalyzer, Funnel, FunnelAnalysisResult } from './funnel-analyzer';
import { attributionModeler, AttributionReport } from './attribution-modeler';
import { db } from './firestore';
interface ConversionDashboardProps {
userId: string;
dateRange: { start: Date; end: Date };
}
export const ConversionDashboard: React.FC<ConversionDashboardProps> = ({ userId, dateRange }) => {
const [goals, setGoals] = useState<Goal[]>([]);
const [funnels, setFunnels] = useState<Funnel[]>([]);
const [selectedFunnel, setSelectedFunnel] = useState<FunnelAnalysisResult | null>(null);
const [attributionReport, setAttributionReport] = useState<AttributionReport | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadDashboardData();
}, [userId, dateRange]);
const loadDashboardData = async () => {
setLoading(true);
// Load goals
await goalTracker.loadGoals();
const goalsSnapshot = await db.collection('goals').get();
const loadedGoals = goalsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as Goal));
setGoals(loadedGoals);
// Load funnels
const funnelsSnapshot = await db.collection('funnels').get();
const loadedFunnels = funnelsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as Funnel));
setFunnels(loadedFunnels);
// Analyze first funnel
if (loadedFunnels.length > 0) {
const analysis = await funnelAnalyzer.analyzeFunnel(loadedFunnels[0]);
setSelectedFunnel(analysis);
}
// Generate attribution report
const conversionsSnapshot = await db.collection('conversions')
.where('timestamp', '>=', dateRange.start.getTime())
.where('timestamp', '<=', dateRange.end.getTime())
.get();
const conversions = conversionsSnapshot.docs.map(doc => doc.data());
const report = await attributionModeler.generateReport(conversions as any[], 'position_based');
setAttributionReport(report);
setLoading(false);
};
if (loading) {
return <div className="loading">Loading conversion analytics...</div>;
}
return (
<div className="conversion-dashboard">
<h1>Conversion Analytics</h1>
{/* Goals Overview */}
<section className="goals-section">
<h2>Conversion Goals</h2>
<div className="goals-grid">
{goals.map(goal => (
<GoalCard key={goal.id} goal={goal} dateRange={dateRange} />
))}
</div>
</section>
{/* Funnel Visualization */}
{selectedFunnel && (
<section className="funnel-section">
<h2>Funnel Analysis: {selectedFunnel.funnelName}</h2>
<div className="funnel-stats">
<div className="stat">
<label>Total Users:</label>
<span>{selectedFunnel.totalUsers.toLocaleString()}</span>
</div>
<div className="stat">
<label>Conversion Rate:</label>
<span>{selectedFunnel.conversionRate.toFixed(2)}%</span>
</div>
<div className="stat">
<label>Avg Time to Convert:</label>
<span>{formatDuration(selectedFunnel.averageTimeToConvert)}</span>
</div>
</div>
<div className="funnel-steps">
{selectedFunnel.stepResults.map((step, index) => (
<FunnelStepCard key={step.stepId} step={step} index={index} />
))}
</div>
</section>
)}
{/* Attribution Report */}
{attributionReport && (
<section className="attribution-section">
<h2>Attribution Analysis (Position-Based Model)</h2>
<div className="attribution-stats">
<div className="stat">
<label>Total Conversions:</label>
<span>{attributionReport.totalConversions.toLocaleString()}</span>
</div>
<div className="stat">
<label>Total Value:</label>
<span>${attributionReport.totalValue.toLocaleString()}</span>
</div>
</div>
<div className="attribution-breakdown">
<h3>Credit by Touchpoint Type</h3>
{Array.from(attributionReport.creditByType.entries()).map(([type, value]) => (
<div key={type} className="attribution-item">
<span className="type">{type}</span>
<span className="value">${value.toFixed(2)}</span>
<div className="bar" style={{ width: `${(value / attributionReport.totalValue) * 100}%` }} />
</div>
))}
</div>
</section>
)}
</div>
);
};
const GoalCard: React.FC<{ goal: Goal; dateRange: { start: Date; end: Date } }> = ({ goal, dateRange }) => {
const [conversionRate, setConversionRate] = useState<number>(0);
useEffect(() => {
goalTracker.getConversionRate(goal.id, dateRange.start, dateRange.end)
.then(rate => setConversionRate(rate));
}, [goal.id, dateRange]);
return (
<div className="goal-card">
<h3>{goal.name}</h3>
<div className="goal-type">{goal.type}</div>
<div className="goal-value">${goal.value.toFixed(2)}</div>
<div className="conversion-rate">{(conversionRate * 100).toFixed(2)}% CR</div>
</div>
);
};
const FunnelStepCard: React.FC<{ step: any; index: number }> = ({ step, index }) => {
return (
<div className="funnel-step">
<div className="step-number">{index + 1}</div>
<div className="step-name">{step.stepName}</div>
<div className="step-metrics">
<div className="metric">
<label>Entered:</label>
<span>{step.usersEntered.toLocaleString()}</span>
</div>
<div className="metric">
<label>Completed:</label>
<span>{step.usersCompleted.toLocaleString()}</span>
</div>
<div className="metric">
<label>Completion Rate:</label>
<span>{step.completionRate.toFixed(1)}%</span>
</div>
<div className="metric">
<label>Drop-off:</label>
<span className="drop-off">{step.dropOffRate.toFixed(1)}%</span>
</div>
</div>
<div className="progress-bar">
<div className="completed" style={{ width: `${step.completionRate}%` }} />
</div>
</div>
);
};
const formatDuration = (ms: number): string => {
const minutes = Math.floor(ms / 60000);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}d ${hours % 24}h`;
if (hours > 0) return `${hours}h ${minutes % 60}m`;
return `${minutes}m`;
};
ROI Calculator
Calculate return on investment for conversion optimization efforts by comparing costs (development time, testing infrastructure) against revenue gains from improved conversion rates.
// roi-calculator.ts - ROI Calculator for Conversion Optimization
export interface CROInvestment {
developmentHours: number;
hourlyRate: number;
toolingCosts: number;
testDuration: number; // days
}
export interface ConversionImprovement {
baselineConversionRate: number; // percentage
optimizedConversionRate: number; // percentage
monthlyTraffic: number; // number of users
averageOrderValue: number; // dollars
}
export interface ROICalculation {
totalInvestment: number;
monthlyRevenueLift: number;
annualRevenueLift: number;
roi: number; // percentage
paybackPeriod: number; // months
breakEvenDate: Date;
}
class ROICalculator {
calculateCROROI(investment: CROInvestment, improvement: ConversionImprovement): ROICalculation {
// Calculate total investment
const totalInvestment =
(investment.developmentHours * investment.hourlyRate) +
investment.toolingCosts;
// Calculate baseline revenue
const baselineMonthlyConversions =
(improvement.monthlyTraffic * improvement.baselineConversionRate) / 100;
const baselineMonthlyRevenue =
baselineMonthlyConversions * improvement.averageOrderValue;
// Calculate optimized revenue
const optimizedMonthlyConversions =
(improvement.monthlyTraffic * improvement.optimizedConversionRate) / 100;
const optimizedMonthlyRevenue =
optimizedMonthlyConversions * improvement.averageOrderValue;
// Calculate lift
const monthlyRevenueLift = optimizedMonthlyRevenue - baselineMonthlyRevenue;
const annualRevenueLift = monthlyRevenueLift * 12;
// Calculate ROI
const roi = ((annualRevenueLift - totalInvestment) / totalInvestment) * 100;
// Calculate payback period
const paybackPeriod = totalInvestment / monthlyRevenueLift;
// Calculate break-even date
const breakEvenDate = new Date();
breakEvenDate.setMonth(breakEvenDate.getMonth() + Math.ceil(paybackPeriod));
return {
totalInvestment,
monthlyRevenueLift,
annualRevenueLift,
roi,
paybackPeriod,
breakEvenDate
};
}
generateReport(calculation: ROICalculation): string {
return `
CRO ROI Analysis
================
Investment: $${calculation.totalInvestment.toLocaleString()}
Revenue Impact:
- Monthly Lift: $${calculation.monthlyRevenueLift.toLocaleString()}
- Annual Lift: $${calculation.annualRevenueLift.toLocaleString()}
ROI: ${calculation.roi.toFixed(1)}%
Payback Period: ${calculation.paybackPeriod.toFixed(1)} months
Break-even Date: ${calculation.breakEvenDate.toLocaleDateString()}
${calculation.roi > 100 ? '✓ Strong ROI - Highly recommended' : '⚠ Marginal ROI - Reconsider approach'}
`.trim();
}
}
export const roiCalculator = new ROICalculator();
Conclusion
Comprehensive conversion tracking transforms ChatGPT apps from experimental tools into revenue-generating products. By implementing goal configuration that captures meaningful user actions, funnel visualization that reveals drop-off points, attribution modeling that credits the right touchpoints, and CRO frameworks that systematically improve conversion rates, you build a data-driven optimization engine.
The production-ready code examples in this article provide a complete conversion tracking system: goal trackers that monitor completion events with cooldown logic, funnel analyzers that calculate step completion rates and drop-off percentages, attribution modelers that distribute conversion credit across touchpoints using multiple models, CRO experiment trackers that run statistically rigorous A/B tests, journey mappers that visualize complete customer paths, conversion dashboards that present metrics in actionable formats, and ROI calculators that justify optimization investments.
Ready to implement conversion tracking in your ChatGPT app? Start building with MakeAIHQ and deploy comprehensive conversion analytics in minutes. Our platform includes pre-built conversion tracking templates, funnel analysis tools, and attribution reporting dashboards specifically designed for ChatGPT apps—no complex analytics setup required.
Related Resources
- Analytics Dashboard: Real-Time Metrics & Visualizations - Build comprehensive analytics dashboards for ChatGPT apps
- Funnel Analysis: Multi-Step Conversion Tracking - Deep dive into funnel optimization strategies
- A/B Testing: Experiment Design & Statistical Analysis - Statistical frameworks for conversion optimization
- User Properties: Custom Attributes & Segmentation - Track user attributes for better conversion analysis
- ROI Calculator: Measure ChatGPT App Revenue Impact - Calculate the financial impact of your ChatGPT app
- ChatGPT App Analytics Guide - Complete guide to ChatGPT app analytics
- Google Analytics 4 Conversions Guide - Official GA4 conversion tracking documentation
- Attribution Modeling Best Practices - Google's guide to attribution models
- Conversion Rate Optimization Strategies - CXL's comprehensive CRO guide
About MakeAIHQ: We're the no-code platform for building and deploying ChatGPT apps to the OpenAI App Store. From zero to production in 48 hours—no coding required. Start your free trial today.