kanmi-perf-revenue 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +251 -0
- package/dist/empirical/conversion-curve.d.ts +95 -0
- package/dist/empirical/conversion-curve.d.ts.map +1 -0
- package/dist/empirical/conversion-curve.js +240 -0
- package/dist/empirical/conversion-curve.js.map +1 -0
- package/dist/empirical/data-import.d.ts +59 -0
- package/dist/empirical/data-import.d.ts.map +1 -0
- package/dist/empirical/data-import.js +186 -0
- package/dist/empirical/data-import.js.map +1 -0
- package/dist/empirical/datadog-session-query.d.ts +66 -0
- package/dist/empirical/datadog-session-query.d.ts.map +1 -0
- package/dist/empirical/datadog-session-query.js +260 -0
- package/dist/empirical/datadog-session-query.js.map +1 -0
- package/dist/empirical/index.d.ts +109 -0
- package/dist/empirical/index.d.ts.map +1 -0
- package/dist/empirical/index.js +267 -0
- package/dist/empirical/index.js.map +1 -0
- package/dist/empirical/opportunity-calculator.d.ts +107 -0
- package/dist/empirical/opportunity-calculator.d.ts.map +1 -0
- package/dist/empirical/opportunity-calculator.js +238 -0
- package/dist/empirical/opportunity-calculator.js.map +1 -0
- package/dist/empirical/report.d.ts +37 -0
- package/dist/empirical/report.d.ts.map +1 -0
- package/dist/empirical/report.js +264 -0
- package/dist/empirical/report.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Empirical Performance Revenue Engine
|
|
4
|
+
*
|
|
5
|
+
* A simpler, more transparent approach to performance-revenue analysis.
|
|
6
|
+
* Uses actual session-level data to build empirical conversion curves.
|
|
7
|
+
*
|
|
8
|
+
* Key differences from coefficient-based models:
|
|
9
|
+
* - No assumed "0.5% CVR per 100ms LCP" coefficients
|
|
10
|
+
* - Measures actual CVR at each performance bucket from YOUR data
|
|
11
|
+
* - More honest about what we know vs. assume
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* // With Datadog credentials
|
|
15
|
+
* const result = await analyzeWithDatadog({
|
|
16
|
+
* apiKey: 'xxx',
|
|
17
|
+
* appKey: 'xxx',
|
|
18
|
+
* startDate: '2024-01-01',
|
|
19
|
+
* endDate: '2024-01-14',
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // With mock data (for testing)
|
|
23
|
+
* const result = analyzeWithMockData({
|
|
24
|
+
* sessionCount: 50000,
|
|
25
|
+
* startDate: '2024-01-01',
|
|
26
|
+
* endDate: '2024-01-14',
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* @author Kanmi Obasa <i@kanmiobasa.com>
|
|
30
|
+
*/
|
|
31
|
+
import { queryDatadogSessions, generateMockSessions, } from './datadog-session-query.js';
|
|
32
|
+
import { buildAllCurves } from './conversion-curve.js';
|
|
33
|
+
import { calculateAllOpportunities, getTopOpportunity, } from './opportunity-calculator.js';
|
|
34
|
+
import { generateReport } from './report.js';
|
|
35
|
+
import { importFromFile } from './data-import.js';
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// MAIN ANALYSIS FUNCTIONS
|
|
38
|
+
// =============================================================================
|
|
39
|
+
/**
|
|
40
|
+
* Run full analysis using Datadog RUM data.
|
|
41
|
+
*/
|
|
42
|
+
export async function analyzeWithDatadog(options) {
|
|
43
|
+
// Query Datadog for session data
|
|
44
|
+
const queryResult = await queryDatadogSessions(options);
|
|
45
|
+
return processSessionData(queryResult, {
|
|
46
|
+
clientName: options.clientName,
|
|
47
|
+
period: queryResult.metadata.period,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Run analysis with mock data (for testing/demo).
|
|
52
|
+
*/
|
|
53
|
+
export function analyzeWithMockData(options) {
|
|
54
|
+
// Generate mock session data
|
|
55
|
+
const queryResult = generateMockSessions({
|
|
56
|
+
count: options.sessionCount,
|
|
57
|
+
startDate: options.startDate,
|
|
58
|
+
endDate: options.endDate,
|
|
59
|
+
baseConversionRate: options.baseConversionRate,
|
|
60
|
+
});
|
|
61
|
+
return processSessionData(queryResult, {
|
|
62
|
+
clientName: options.clientName,
|
|
63
|
+
period: queryResult.metadata.period,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Run analysis from CSV or JSON file.
|
|
68
|
+
* Bring your own data from any source.
|
|
69
|
+
*/
|
|
70
|
+
export function analyzeFromFile(options) {
|
|
71
|
+
const queryResult = importFromFile(options.filePath, {
|
|
72
|
+
startDate: options.startDate,
|
|
73
|
+
endDate: options.endDate,
|
|
74
|
+
});
|
|
75
|
+
return processSessionData(queryResult, {
|
|
76
|
+
clientName: options.clientName,
|
|
77
|
+
period: queryResult.metadata.period,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Process session data through the full analysis pipeline.
|
|
82
|
+
*/
|
|
83
|
+
function processSessionData(queryResult, config) {
|
|
84
|
+
const { sessions, metadata } = queryResult;
|
|
85
|
+
// Build empirical conversion curves
|
|
86
|
+
const curves = buildAllCurves(sessions);
|
|
87
|
+
// Calculate opportunities
|
|
88
|
+
const opportunities = calculateAllOpportunities(curves, {
|
|
89
|
+
periodDays: metadata.period.days,
|
|
90
|
+
});
|
|
91
|
+
// Find top opportunity
|
|
92
|
+
const topOpp = getTopOpportunity(opportunities);
|
|
93
|
+
const topOpportunity = topOpp
|
|
94
|
+
? {
|
|
95
|
+
metric: topOpp.metric,
|
|
96
|
+
monthlyRevenue: topOpp.scenario.conservative_monthly_revenue,
|
|
97
|
+
summary: opportunities[topOpp.metric].recommendation.summary,
|
|
98
|
+
}
|
|
99
|
+
: null;
|
|
100
|
+
// Generate report
|
|
101
|
+
const reportConfig = {
|
|
102
|
+
clientName: config.clientName,
|
|
103
|
+
period: config.period,
|
|
104
|
+
};
|
|
105
|
+
const report = generateReport({ curves, opportunities }, reportConfig);
|
|
106
|
+
return {
|
|
107
|
+
curves,
|
|
108
|
+
opportunities,
|
|
109
|
+
topOpportunity,
|
|
110
|
+
report,
|
|
111
|
+
metadata: {
|
|
112
|
+
totalSessions: metadata.total_sessions,
|
|
113
|
+
sessionsWithLcp: metadata.sessions_with_lcp,
|
|
114
|
+
sessionsWithConversions: metadata.sessions_with_conversions,
|
|
115
|
+
period: metadata.period,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// CLI ENTRY POINT
|
|
121
|
+
// =============================================================================
|
|
122
|
+
/**
|
|
123
|
+
* CLI entry point.
|
|
124
|
+
*/
|
|
125
|
+
export async function main() {
|
|
126
|
+
const args = process.argv.slice(2);
|
|
127
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
128
|
+
printHelp();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Check for file import mode
|
|
132
|
+
const filePath = getArg(args, '--file');
|
|
133
|
+
const clientName = getArg(args, '--client');
|
|
134
|
+
if (filePath) {
|
|
135
|
+
console.log(`Importing data from ${filePath}...\n`);
|
|
136
|
+
try {
|
|
137
|
+
const result = analyzeFromFile({
|
|
138
|
+
filePath,
|
|
139
|
+
clientName,
|
|
140
|
+
startDate: getArg(args, '--start'),
|
|
141
|
+
endDate: getArg(args, '--end'),
|
|
142
|
+
});
|
|
143
|
+
console.log(result.report);
|
|
144
|
+
printSummary(result);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error('Import failed:', error);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Check for demo mode
|
|
153
|
+
const demoMode = args.includes('--demo');
|
|
154
|
+
if (demoMode) {
|
|
155
|
+
console.log('Running in demo mode with mock data...\n');
|
|
156
|
+
const result = analyzeWithMockData({
|
|
157
|
+
sessionCount: 50000,
|
|
158
|
+
startDate: '2024-01-01',
|
|
159
|
+
endDate: '2024-01-14',
|
|
160
|
+
clientName: clientName || 'Demo Company',
|
|
161
|
+
baseConversionRate: 0.03,
|
|
162
|
+
});
|
|
163
|
+
console.log(result.report);
|
|
164
|
+
printSummary(result);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Check for Datadog credentials
|
|
168
|
+
const apiKey = process.env.DD_API_KEY || getArg(args, '--api-key');
|
|
169
|
+
const appKey = process.env.DD_APP_KEY || getArg(args, '--app-key');
|
|
170
|
+
const startDate = getArg(args, '--start') || getDefaultStartDate();
|
|
171
|
+
const endDate = getArg(args, '--end') || getDefaultEndDate();
|
|
172
|
+
if (!apiKey || !appKey) {
|
|
173
|
+
console.error('Error: Datadog credentials required.');
|
|
174
|
+
console.error('Set DD_API_KEY and DD_APP_KEY environment variables,');
|
|
175
|
+
console.error('or use --api-key and --app-key arguments.');
|
|
176
|
+
console.error('\nOr run with --demo for mock data demo.\n');
|
|
177
|
+
printHelp();
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
console.log(`Analyzing ${startDate} to ${endDate}...\n`);
|
|
181
|
+
try {
|
|
182
|
+
const result = await analyzeWithDatadog({
|
|
183
|
+
apiKey,
|
|
184
|
+
appKey,
|
|
185
|
+
startDate,
|
|
186
|
+
endDate,
|
|
187
|
+
clientName,
|
|
188
|
+
});
|
|
189
|
+
console.log(result.report);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
console.error('Analysis failed:', error);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// =============================================================================
|
|
197
|
+
// HELPERS
|
|
198
|
+
// =============================================================================
|
|
199
|
+
function printHelp() {
|
|
200
|
+
console.log(`
|
|
201
|
+
Empirical Performance Revenue Engine
|
|
202
|
+
|
|
203
|
+
Usage:
|
|
204
|
+
npx tsx src/empirical/index.ts [options]
|
|
205
|
+
|
|
206
|
+
Data Sources (choose one):
|
|
207
|
+
--file PATH Import from CSV or JSON file (recommended)
|
|
208
|
+
--demo Run with mock data (for testing)
|
|
209
|
+
--api-key KEY Datadog API key (or set DD_API_KEY env var)
|
|
210
|
+
|
|
211
|
+
Options:
|
|
212
|
+
--start DATE Start date (YYYY-MM-DD). Default: 14 days ago
|
|
213
|
+
--end DATE End date (YYYY-MM-DD). Default: today
|
|
214
|
+
--client NAME Client name for report header
|
|
215
|
+
--help, -h Show this help message
|
|
216
|
+
|
|
217
|
+
Examples:
|
|
218
|
+
# Import from your own CSV/JSON
|
|
219
|
+
npx tsx src/empirical/index.ts --file sessions.csv --client "Acme Corp"
|
|
220
|
+
|
|
221
|
+
# Demo with mock data
|
|
222
|
+
npx tsx src/empirical/index.ts --demo
|
|
223
|
+
|
|
224
|
+
# With Datadog credentials
|
|
225
|
+
DD_API_KEY=xxx DD_APP_KEY=xxx npx tsx src/empirical/index.ts
|
|
226
|
+
|
|
227
|
+
CSV/JSON Format:
|
|
228
|
+
Required columns: session_id, converted (or has_purchase)
|
|
229
|
+
Optional: lcp_ms, inp_ms, cls, order_value, device, page_type
|
|
230
|
+
`);
|
|
231
|
+
}
|
|
232
|
+
function printSummary(result) {
|
|
233
|
+
console.log('\n--- Analysis Summary ---\n');
|
|
234
|
+
console.log(`Total Sessions: ${result.metadata.totalSessions.toLocaleString()}`);
|
|
235
|
+
console.log(`Sessions with LCP: ${result.metadata.sessionsWithLcp.toLocaleString()}`);
|
|
236
|
+
console.log(`Conversions: ${result.metadata.sessionsWithConversions.toLocaleString()}`);
|
|
237
|
+
if (result.topOpportunity) {
|
|
238
|
+
console.log(`\nTop Opportunity: ${result.topOpportunity.metric.toUpperCase()}`);
|
|
239
|
+
console.log(`Monthly Revenue: $${result.topOpportunity.monthlyRevenue.toLocaleString()}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function getArg(args, flag) {
|
|
243
|
+
const index = args.indexOf(flag);
|
|
244
|
+
if (index !== -1 && args[index + 1]) {
|
|
245
|
+
return args[index + 1];
|
|
246
|
+
}
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
function getDefaultStartDate() {
|
|
250
|
+
const date = new Date();
|
|
251
|
+
date.setDate(date.getDate() - 14);
|
|
252
|
+
return date.toISOString().split('T')[0];
|
|
253
|
+
}
|
|
254
|
+
function getDefaultEndDate() {
|
|
255
|
+
return new Date().toISOString().split('T')[0];
|
|
256
|
+
}
|
|
257
|
+
// Re-export functions for library use
|
|
258
|
+
export { queryDatadogSessions, generateMockSessions } from './datadog-session-query.js';
|
|
259
|
+
export { buildConversionCurve, buildAllCurves, buildSegmentedCurves } from './conversion-curve.js';
|
|
260
|
+
export { calculateOpportunity, calculateAllOpportunities, getTopOpportunity } from './opportunity-calculator.js';
|
|
261
|
+
export { generateReport } from './report.js';
|
|
262
|
+
export { importFromFile, importFromCsv, importFromJson } from './data-import.js';
|
|
263
|
+
// Run if executed directly
|
|
264
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
265
|
+
main().catch(console.error);
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/empirical/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GAGrB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,cAAc,EAAwB,MAAM,uBAAuB,CAAC;AAC7E,OAAO,EACL,yBAAyB,EACzB,iBAAiB,GAElB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,cAAc,EAAqB,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAuDlD,gFAAgF;AAChF,0BAA0B;AAC1B,gFAAgF;AAEhF;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAA+B;IAE/B,iCAAiC;IACjC,MAAM,WAAW,GAAG,MAAM,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAExD,OAAO,kBAAkB,CAAC,WAAW,EAAE;QACrC,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,MAAM;KACpC,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAA4B;IAC9D,6BAA6B;IAC7B,MAAM,WAAW,GAAG,oBAAoB,CAAC;QACvC,KAAK,EAAE,OAAO,CAAC,YAAY;QAC3B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,kBAAkB,EAAE,OAAO,CAAC,kBAAkB;KAC/C,CAAC,CAAC;IAEH,OAAO,kBAAkB,CAAC,WAAW,EAAE;QACrC,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,MAAM;KACpC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,OAA4B;IAC1D,MAAM,WAAW,GAAG,cAAc,CAAC,OAAO,CAAC,QAAQ,EAAE;QACnD,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAC,CAAC;IAEH,OAAO,kBAAkB,CAAC,WAAW,EAAE;QACrC,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,MAAM;KACpC,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB,CACzB,WAAwB,EACxB,MAGC;IAED,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,WAAW,CAAC;IAE3C,oCAAoC;IACpC,MAAM,MAAM,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;IAExC,0BAA0B;IAC1B,MAAM,aAAa,GAAG,yBAAyB,CAAC,MAAM,EAAE;QACtD,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI;KACjC,CAAC,CAAC;IAEH,uBAAuB;IACvB,MAAM,MAAM,GAAG,iBAAiB,CAAC,aAAa,CAAC,CAAC;IAChD,MAAM,cAAc,GAAG,MAAM;QAC3B,CAAC,CAAC;YACE,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,cAAc,EAAE,MAAM,CAAC,QAAQ,CAAC,4BAA4B;YAC5D,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,OAAO;SAC7D;QACH,CAAC,CAAC,IAAI,CAAC;IAET,kBAAkB;IAClB,MAAM,YAAY,GAAiB;QACjC,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,MAAM,EAAE,MAAM,CAAC,MAAM;KACtB,CAAC;IAEF,MAAM,MAAM,GAAG,cAAc,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,EAAE,YAAY,CAAC,CAAC;IAEvE,OAAO;QACL,MAAM;QACN,aAAa;QACb,cAAc;QACd,MAAM;QACN,QAAQ,EAAE;YACR,aAAa,EAAE,QAAQ,CAAC,cAAc;YACtC,eAAe,EAAE,QAAQ,CAAC,iBAAiB;YAC3C,uBAAuB,EAAE,QAAQ,CAAC,yBAAyB;YAC3D,MAAM,EAAE,QAAQ,CAAC,MAAM;SACxB;KACF,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,kBAAkB;AAClB,gFAAgF;AAEhF;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,IAAI;IACxB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAEnC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,SAAS,EAAE,CAAC;QACZ,OAAO;IACT,CAAC;IAED,6BAA6B;IAC7B,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACxC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAE5C,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,CAAC,GAAG,CAAC,uBAAuB,QAAQ,OAAO,CAAC,CAAC;QAEpD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,eAAe,CAAC;gBAC7B,QAAQ;gBACR,UAAU;gBACV,SAAS,EAAE,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC;gBAClC,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC;aAC/B,CAAC,CAAC;YAEH,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC3B,YAAY,CAAC,MAAM,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC;YACvC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;IAED,sBAAsB;IACtB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAEzC,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;QAExD,MAAM,MAAM,GAAG,mBAAmB,CAAC;YACjC,YAAY,EAAE,KAAK;YACnB,SAAS,EAAE,YAAY;YACvB,OAAO,EAAE,YAAY;YACrB,UAAU,EAAE,UAAU,IAAI,cAAc;YACxC,kBAAkB,EAAE,IAAI;SACzB,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3B,YAAY,CAAC,MAAM,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IAED,gCAAgC;IAChC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACnE,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACnE,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,mBAAmB,EAAE,CAAC;IACnE,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,iBAAiB,EAAE,CAAC;IAE7D,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QACvB,OAAO,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;QACtD,OAAO,CAAC,KAAK,CAAC,sDAAsD,CAAC,CAAC;QACtE,OAAO,CAAC,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAC3D,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;QAC5D,SAAS,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,aAAa,SAAS,OAAO,OAAO,OAAO,CAAC,CAAC;IAEzD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,kBAAkB,CAAC;YACtC,MAAM;YACN,MAAM;YACN,SAAS;YACT,OAAO;YACP,UAAU;SACX,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;QACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,gFAAgF;AAChF,UAAU;AACV,gFAAgF;AAEhF,SAAS,SAAS;IAChB,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8Bb,CAAC,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,MAAsB;IAC1C,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,CAAC,mBAAmB,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IACjF,OAAO,CAAC,GAAG,CAAC,sBAAsB,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IACtF,OAAO,CAAC,GAAG,CAAC,gBAAgB,MAAM,CAAC,QAAQ,CAAC,uBAAuB,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IAExF,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;QAC1B,OAAO,CAAC,GAAG,CAAC,sBAAsB,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAChF,OAAO,CAAC,GAAG,CAAC,qBAAqB,MAAM,CAAC,cAAc,CAAC,cAAc,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IAC5F,CAAC;AACH,CAAC;AAED,SAAS,MAAM,CAAC,IAAc,EAAE,IAAY;IAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC;QACpC,OAAO,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,mBAAmB;IAC1B,MAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;IACxB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,iBAAiB;IACxB,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAChD,CAAC;AAYD,sCAAsC;AACtC,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACxF,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AACnG,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACjH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEjF,2BAA2B;AAC3B,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,UAAU,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACpD,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revenue Opportunity Calculator
|
|
3
|
+
*
|
|
4
|
+
* Calculates revenue opportunity from distribution shifts.
|
|
5
|
+
* Given an empirical conversion curve, we calculate:
|
|
6
|
+
* "If X sessions move from slow bucket to fast bucket, revenue increases by $Y"
|
|
7
|
+
*
|
|
8
|
+
* This is based on MEASURED CVR differences, not assumed coefficients.
|
|
9
|
+
*
|
|
10
|
+
* @author Kanmi Obasa <i@kanmiobasa.com>
|
|
11
|
+
*/
|
|
12
|
+
import type { ConversionCurve } from './conversion-curve.js';
|
|
13
|
+
export interface OpportunityScenario {
|
|
14
|
+
/** Descriptive name for the scenario */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Target metric value in ms (sessions above this are "slow") */
|
|
17
|
+
target_ms: number;
|
|
18
|
+
/** Current sessions above target (slow) */
|
|
19
|
+
slow_sessions: number;
|
|
20
|
+
/** Current sessions at or below target (fast) */
|
|
21
|
+
fast_sessions: number;
|
|
22
|
+
/** Measured CVR for slow sessions */
|
|
23
|
+
slow_cvr: number;
|
|
24
|
+
/** Measured CVR for fast sessions */
|
|
25
|
+
fast_cvr: number;
|
|
26
|
+
/** Absolute CVR difference (fast - slow) */
|
|
27
|
+
cvr_delta: number;
|
|
28
|
+
/** Relative CVR improvement ((fast - slow) / slow) */
|
|
29
|
+
relative_improvement: number;
|
|
30
|
+
/** Additional conversions if ALL slow sessions became fast */
|
|
31
|
+
max_additional_conversions: number;
|
|
32
|
+
/** Additional monthly revenue (max scenario) */
|
|
33
|
+
max_monthly_revenue: number;
|
|
34
|
+
/** Conservative estimate (50% of sessions improved) */
|
|
35
|
+
conservative_monthly_revenue: number;
|
|
36
|
+
/** Average order value used */
|
|
37
|
+
aov: number;
|
|
38
|
+
/** Statistical confidence */
|
|
39
|
+
confidence: 'high' | 'medium' | 'low';
|
|
40
|
+
}
|
|
41
|
+
export interface OpportunityReport {
|
|
42
|
+
/** The metric analyzed */
|
|
43
|
+
metric: 'lcp' | 'inp' | 'cls';
|
|
44
|
+
/** Analysis period */
|
|
45
|
+
period: {
|
|
46
|
+
days: number;
|
|
47
|
+
monthly_factor: number;
|
|
48
|
+
};
|
|
49
|
+
/** Scenarios at different target thresholds */
|
|
50
|
+
scenarios: OpportunityScenario[];
|
|
51
|
+
/** Recommended priority based on opportunity size */
|
|
52
|
+
recommendation: {
|
|
53
|
+
priority: 'high' | 'medium' | 'low';
|
|
54
|
+
target_ms: number;
|
|
55
|
+
monthly_opportunity: number;
|
|
56
|
+
summary: string;
|
|
57
|
+
};
|
|
58
|
+
/** Raw curve data for transparency */
|
|
59
|
+
curve_summary: {
|
|
60
|
+
total_sessions: number;
|
|
61
|
+
overall_cvr: number;
|
|
62
|
+
overall_aov: number;
|
|
63
|
+
cvr_range: number;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export interface OpportunityConfig {
|
|
67
|
+
/** Period in days for the data (for monthly normalization) */
|
|
68
|
+
periodDays: number;
|
|
69
|
+
/** Custom target thresholds to evaluate. Default: CWV thresholds */
|
|
70
|
+
targetThresholds?: number[];
|
|
71
|
+
/** Minimum sessions to consider scenario valid. Default: 50 */
|
|
72
|
+
minSessionsForScenario?: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Calculate revenue opportunity from an empirical conversion curve.
|
|
76
|
+
*
|
|
77
|
+
* The core calculation:
|
|
78
|
+
* 1. Count sessions above target threshold (slow)
|
|
79
|
+
* 2. Get measured CVR for fast vs slow buckets
|
|
80
|
+
* 3. Calculate: additional_conversions = slow_sessions × (fast_cvr - slow_cvr)
|
|
81
|
+
* 4. Calculate: revenue = additional_conversions × AOV
|
|
82
|
+
*/
|
|
83
|
+
export declare function calculateOpportunity(curve: ConversionCurve, config: OpportunityConfig): OpportunityReport;
|
|
84
|
+
/**
|
|
85
|
+
* Calculate opportunity for all CWV metrics.
|
|
86
|
+
*/
|
|
87
|
+
export declare function calculateAllOpportunities(curves: {
|
|
88
|
+
lcp: ConversionCurve;
|
|
89
|
+
inp: ConversionCurve;
|
|
90
|
+
cls: ConversionCurve;
|
|
91
|
+
}, config: OpportunityConfig): {
|
|
92
|
+
lcp: OpportunityReport;
|
|
93
|
+
inp: OpportunityReport;
|
|
94
|
+
cls: OpportunityReport;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Get the single highest-impact opportunity across all metrics.
|
|
98
|
+
*/
|
|
99
|
+
export declare function getTopOpportunity(opportunities: {
|
|
100
|
+
lcp: OpportunityReport;
|
|
101
|
+
inp: OpportunityReport;
|
|
102
|
+
cls: OpportunityReport;
|
|
103
|
+
}): {
|
|
104
|
+
metric: 'lcp' | 'inp' | 'cls';
|
|
105
|
+
scenario: OpportunityScenario;
|
|
106
|
+
} | null;
|
|
107
|
+
//# sourceMappingURL=opportunity-calculator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opportunity-calculator.d.ts","sourceRoot":"","sources":["../../src/empirical/opportunity-calculator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAoB,MAAM,uBAAuB,CAAC;AAM/E,MAAM,WAAW,mBAAmB;IAClC,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,aAAa,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,aAAa,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,oBAAoB,EAAE,MAAM,CAAC;IAC7B,8DAA8D;IAC9D,0BAA0B,EAAE,MAAM,CAAC;IACnC,gDAAgD;IAChD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,uDAAuD;IACvD,4BAA4B,EAAE,MAAM,CAAC;IACrC,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,6BAA6B;IAC7B,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;CACvC;AAED,MAAM,WAAW,iBAAiB;IAChC,0BAA0B;IAC1B,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;IAC9B,sBAAsB;IACtB,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,+CAA+C;IAC/C,SAAS,EAAE,mBAAmB,EAAE,CAAC;IACjC,qDAAqD;IACrD,cAAc,EAAE;QACd,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;QACpC,SAAS,EAAE,MAAM,CAAC;QAClB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,sCAAsC;IACtC,aAAa,EAAE;QACb,cAAc,EAAE,MAAM,CAAC;QACvB,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,iBAAiB;IAChC,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,+DAA+D;IAC/D,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAQD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,eAAe,EACtB,MAAM,EAAE,iBAAiB,GACxB,iBAAiB,CA6CnB;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE;IAAE,GAAG,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,eAAe,CAAA;CAAE,EAC5E,MAAM,EAAE,iBAAiB,GACxB;IAAE,GAAG,EAAE,iBAAiB,CAAC;IAAC,GAAG,EAAE,iBAAiB,CAAC;IAAC,GAAG,EAAE,iBAAiB,CAAA;CAAE,CAM5E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,aAAa,EAAE;IAAE,GAAG,EAAE,iBAAiB,CAAC;IAAC,GAAG,EAAE,iBAAiB,CAAC;IAAC,GAAG,EAAE,iBAAiB,CAAA;CAAE,GACxF;IAAE,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;IAAC,QAAQ,EAAE,mBAAmB,CAAA;CAAE,GAAG,IAAI,CAoBzE"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revenue Opportunity Calculator
|
|
3
|
+
*
|
|
4
|
+
* Calculates revenue opportunity from distribution shifts.
|
|
5
|
+
* Given an empirical conversion curve, we calculate:
|
|
6
|
+
* "If X sessions move from slow bucket to fast bucket, revenue increases by $Y"
|
|
7
|
+
*
|
|
8
|
+
* This is based on MEASURED CVR differences, not assumed coefficients.
|
|
9
|
+
*
|
|
10
|
+
* @author Kanmi Obasa <i@kanmiobasa.com>
|
|
11
|
+
*/
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// OPPORTUNITY CALCULATOR
|
|
14
|
+
// =============================================================================
|
|
15
|
+
const STANDARD_MONTH_DAYS = 30;
|
|
16
|
+
/**
|
|
17
|
+
* Calculate revenue opportunity from an empirical conversion curve.
|
|
18
|
+
*
|
|
19
|
+
* The core calculation:
|
|
20
|
+
* 1. Count sessions above target threshold (slow)
|
|
21
|
+
* 2. Get measured CVR for fast vs slow buckets
|
|
22
|
+
* 3. Calculate: additional_conversions = slow_sessions × (fast_cvr - slow_cvr)
|
|
23
|
+
* 4. Calculate: revenue = additional_conversions × AOV
|
|
24
|
+
*/
|
|
25
|
+
export function calculateOpportunity(curve, config) {
|
|
26
|
+
const { periodDays, minSessionsForScenario = 50, } = config;
|
|
27
|
+
const monthlyFactor = STANDARD_MONTH_DAYS / periodDays;
|
|
28
|
+
const targets = config.targetThresholds || getDefaultTargets(curve.metric);
|
|
29
|
+
const scenarios = [];
|
|
30
|
+
for (const targetMs of targets) {
|
|
31
|
+
const scenario = calculateScenarioAtTarget(curve, targetMs, monthlyFactor, minSessionsForScenario);
|
|
32
|
+
if (scenario) {
|
|
33
|
+
scenarios.push(scenario);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Sort by opportunity size
|
|
37
|
+
scenarios.sort((a, b) => b.conservative_monthly_revenue - a.conservative_monthly_revenue);
|
|
38
|
+
// Generate recommendation
|
|
39
|
+
const recommendation = generateRecommendation(scenarios, curve);
|
|
40
|
+
return {
|
|
41
|
+
metric: curve.metric,
|
|
42
|
+
period: {
|
|
43
|
+
days: periodDays,
|
|
44
|
+
monthly_factor: monthlyFactor,
|
|
45
|
+
},
|
|
46
|
+
scenarios,
|
|
47
|
+
recommendation,
|
|
48
|
+
curve_summary: {
|
|
49
|
+
total_sessions: curve.summary.total_sessions,
|
|
50
|
+
overall_cvr: curve.summary.overall_cvr,
|
|
51
|
+
overall_aov: curve.summary.overall_aov,
|
|
52
|
+
cvr_range: curve.summary.cvr_range,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Calculate opportunity for all CWV metrics.
|
|
58
|
+
*/
|
|
59
|
+
export function calculateAllOpportunities(curves, config) {
|
|
60
|
+
return {
|
|
61
|
+
lcp: calculateOpportunity(curves.lcp, config),
|
|
62
|
+
inp: calculateOpportunity(curves.inp, config),
|
|
63
|
+
cls: calculateOpportunity(curves.cls, config),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get the single highest-impact opportunity across all metrics.
|
|
68
|
+
*/
|
|
69
|
+
export function getTopOpportunity(opportunities) {
|
|
70
|
+
const allScenarios = [];
|
|
71
|
+
for (const [metric, report] of Object.entries(opportunities)) {
|
|
72
|
+
for (const scenario of report.scenarios) {
|
|
73
|
+
allScenarios.push({
|
|
74
|
+
metric: metric,
|
|
75
|
+
scenario,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (allScenarios.length === 0)
|
|
80
|
+
return null;
|
|
81
|
+
// Sort by conservative revenue opportunity
|
|
82
|
+
allScenarios.sort((a, b) => b.scenario.conservative_monthly_revenue - a.scenario.conservative_monthly_revenue);
|
|
83
|
+
return allScenarios[0];
|
|
84
|
+
}
|
|
85
|
+
// =============================================================================
|
|
86
|
+
// SCENARIO CALCULATION
|
|
87
|
+
// =============================================================================
|
|
88
|
+
function calculateScenarioAtTarget(curve, targetMs, monthlyFactor, minSessions) {
|
|
89
|
+
// Partition buckets into fast (at or below target) and slow (above target)
|
|
90
|
+
const fastBuckets = [];
|
|
91
|
+
const slowBuckets = [];
|
|
92
|
+
for (const bucket of curve.buckets) {
|
|
93
|
+
if (bucket.upper_ms <= targetMs) {
|
|
94
|
+
fastBuckets.push(bucket);
|
|
95
|
+
}
|
|
96
|
+
else if (bucket.lower_ms >= targetMs) {
|
|
97
|
+
slowBuckets.push(bucket);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// Bucket spans the threshold - split proportionally
|
|
101
|
+
const belowPortion = (targetMs - bucket.lower_ms) / (bucket.upper_ms - bucket.lower_ms);
|
|
102
|
+
const abovePortion = 1 - belowPortion;
|
|
103
|
+
if (belowPortion > 0) {
|
|
104
|
+
fastBuckets.push({
|
|
105
|
+
...bucket,
|
|
106
|
+
sessions: Math.round(bucket.sessions * belowPortion),
|
|
107
|
+
conversions: Math.round(bucket.conversions * belowPortion),
|
|
108
|
+
revenue: bucket.revenue * belowPortion,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (abovePortion > 0) {
|
|
112
|
+
slowBuckets.push({
|
|
113
|
+
...bucket,
|
|
114
|
+
sessions: Math.round(bucket.sessions * abovePortion),
|
|
115
|
+
conversions: Math.round(bucket.conversions * abovePortion),
|
|
116
|
+
revenue: bucket.revenue * abovePortion,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const fastSessions = fastBuckets.reduce((sum, b) => sum + b.sessions, 0);
|
|
122
|
+
const slowSessions = slowBuckets.reduce((sum, b) => sum + b.sessions, 0);
|
|
123
|
+
// Need minimum sessions in both groups for valid scenario
|
|
124
|
+
if (fastSessions < minSessions || slowSessions < minSessions) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const fastConversions = fastBuckets.reduce((sum, b) => sum + b.conversions, 0);
|
|
128
|
+
const slowConversions = slowBuckets.reduce((sum, b) => sum + b.conversions, 0);
|
|
129
|
+
const fastCvr = fastConversions / fastSessions;
|
|
130
|
+
const slowCvr = slowConversions / slowSessions;
|
|
131
|
+
const cvrDelta = fastCvr - slowCvr;
|
|
132
|
+
// If slow sessions already convert better, no opportunity
|
|
133
|
+
if (cvrDelta <= 0) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const totalRevenue = fastBuckets.reduce((sum, b) => sum + b.revenue, 0) +
|
|
137
|
+
slowBuckets.reduce((sum, b) => sum + b.revenue, 0);
|
|
138
|
+
const totalConversions = fastConversions + slowConversions;
|
|
139
|
+
const aov = totalConversions > 0 ? totalRevenue / totalConversions : 0;
|
|
140
|
+
// Calculate opportunity
|
|
141
|
+
// If all slow sessions achieved fast CVR, how many additional conversions?
|
|
142
|
+
const maxAdditionalConversions = slowSessions * cvrDelta;
|
|
143
|
+
const maxRevenue = maxAdditionalConversions * aov * monthlyFactor;
|
|
144
|
+
// Conservative: assume only 50% of sessions can realistically be improved
|
|
145
|
+
const conservativeRevenue = maxRevenue * 0.5;
|
|
146
|
+
// Determine confidence
|
|
147
|
+
let confidence = 'low';
|
|
148
|
+
const minBucketSessions = Math.min(...fastBuckets.map((b) => b.sessions), ...slowBuckets.map((b) => b.sessions));
|
|
149
|
+
if (minBucketSessions >= 100) {
|
|
150
|
+
confidence = 'high';
|
|
151
|
+
}
|
|
152
|
+
else if (minBucketSessions >= 30) {
|
|
153
|
+
confidence = 'medium';
|
|
154
|
+
}
|
|
155
|
+
const metricLabel = curve.metric.toUpperCase();
|
|
156
|
+
const targetLabel = formatTargetLabel(targetMs, curve.metric);
|
|
157
|
+
return {
|
|
158
|
+
name: `Improve ${metricLabel} to ${targetLabel}`,
|
|
159
|
+
target_ms: targetMs,
|
|
160
|
+
slow_sessions: slowSessions,
|
|
161
|
+
fast_sessions: fastSessions,
|
|
162
|
+
slow_cvr: slowCvr,
|
|
163
|
+
fast_cvr: fastCvr,
|
|
164
|
+
cvr_delta: cvrDelta,
|
|
165
|
+
relative_improvement: slowCvr > 0 ? cvrDelta / slowCvr : 0,
|
|
166
|
+
max_additional_conversions: Math.round(maxAdditionalConversions * monthlyFactor),
|
|
167
|
+
max_monthly_revenue: Math.round(maxRevenue),
|
|
168
|
+
conservative_monthly_revenue: Math.round(conservativeRevenue),
|
|
169
|
+
aov,
|
|
170
|
+
confidence,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// =============================================================================
|
|
174
|
+
// RECOMMENDATION GENERATION
|
|
175
|
+
// =============================================================================
|
|
176
|
+
function generateRecommendation(scenarios, curve) {
|
|
177
|
+
if (scenarios.length === 0) {
|
|
178
|
+
return {
|
|
179
|
+
priority: 'low',
|
|
180
|
+
target_ms: curve.thresholds.good,
|
|
181
|
+
monthly_opportunity: 0,
|
|
182
|
+
summary: `No significant ${curve.metric.toUpperCase()} opportunity detected. Current performance distribution shows minimal CVR variation.`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// Find the best scenario (highest conservative revenue with good confidence)
|
|
186
|
+
const viableScenarios = scenarios.filter((s) => s.confidence !== 'low');
|
|
187
|
+
const bestScenario = viableScenarios[0] || scenarios[0];
|
|
188
|
+
// Determine priority
|
|
189
|
+
let priority = 'low';
|
|
190
|
+
if (bestScenario.conservative_monthly_revenue >= 10000) {
|
|
191
|
+
priority = 'high';
|
|
192
|
+
}
|
|
193
|
+
else if (bestScenario.conservative_monthly_revenue >= 2500) {
|
|
194
|
+
priority = 'medium';
|
|
195
|
+
}
|
|
196
|
+
const metricLabel = curve.metric.toUpperCase();
|
|
197
|
+
const targetLabel = formatTargetLabel(bestScenario.target_ms, curve.metric);
|
|
198
|
+
const slowPct = ((bestScenario.slow_sessions / curve.summary.total_sessions) * 100).toFixed(0);
|
|
199
|
+
const cvrLiftPct = (bestScenario.relative_improvement * 100).toFixed(1);
|
|
200
|
+
return {
|
|
201
|
+
priority,
|
|
202
|
+
target_ms: bestScenario.target_ms,
|
|
203
|
+
monthly_opportunity: bestScenario.conservative_monthly_revenue,
|
|
204
|
+
summary: `${slowPct}% of sessions have ${metricLabel} > ${targetLabel}. These sessions convert ${cvrLiftPct}% worse than faster sessions. Improving them could generate ~$${formatCurrency(bestScenario.conservative_monthly_revenue)}/month.`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// =============================================================================
|
|
208
|
+
// HELPERS
|
|
209
|
+
// =============================================================================
|
|
210
|
+
function getDefaultTargets(metric) {
|
|
211
|
+
switch (metric) {
|
|
212
|
+
case 'lcp':
|
|
213
|
+
return [2500, 3000, 4000]; // Good threshold, mid, poor threshold
|
|
214
|
+
case 'inp':
|
|
215
|
+
return [200, 300, 500];
|
|
216
|
+
case 'cls':
|
|
217
|
+
return [100, 150, 250]; // Scaled by 1000
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function formatTargetLabel(targetMs, metric) {
|
|
221
|
+
if (metric === 'cls') {
|
|
222
|
+
return (targetMs / 1000).toFixed(2);
|
|
223
|
+
}
|
|
224
|
+
if (targetMs >= 1000) {
|
|
225
|
+
return `${(targetMs / 1000).toFixed(1)}s`;
|
|
226
|
+
}
|
|
227
|
+
return `${targetMs}ms`;
|
|
228
|
+
}
|
|
229
|
+
function formatCurrency(value) {
|
|
230
|
+
if (value >= 1_000_000) {
|
|
231
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
232
|
+
}
|
|
233
|
+
if (value >= 1_000) {
|
|
234
|
+
return `${(value / 1_000).toFixed(0)}K`;
|
|
235
|
+
}
|
|
236
|
+
return value.toFixed(0);
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=opportunity-calculator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opportunity-calculator.js","sourceRoot":"","sources":["../../src/empirical/opportunity-calculator.ts"],"names":[],"mappings":"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"}
|