@contractspec/lib.analytics 1.57.0 → 1.59.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/dist/browser/churn/index.js +77 -0
- package/dist/browser/churn/predictor.js +77 -0
- package/dist/browser/cohort/index.js +117 -0
- package/dist/browser/cohort/tracker.js +117 -0
- package/dist/browser/funnel/analyzer.js +68 -0
- package/dist/browser/funnel/index.js +68 -0
- package/dist/browser/growth/hypothesis-generator.js +46 -0
- package/dist/browser/growth/index.js +46 -0
- package/dist/browser/index.js +723 -0
- package/dist/browser/lifecycle/index.js +287 -0
- package/dist/browser/lifecycle/metric-collectors.js +58 -0
- package/dist/browser/lifecycle/posthog-bridge.js +79 -0
- package/dist/browser/lifecycle/posthog-metric-source.js +205 -0
- package/dist/browser/posthog/event-source.js +138 -0
- package/dist/browser/posthog/index.js +138 -0
- package/dist/browser/types.js +0 -0
- package/dist/churn/index.d.ts +2 -2
- package/dist/churn/index.d.ts.map +1 -0
- package/dist/churn/index.js +77 -2
- package/dist/churn/predictor.d.ts +15 -19
- package/dist/churn/predictor.d.ts.map +1 -1
- package/dist/churn/predictor.js +72 -68
- package/dist/cohort/index.d.ts +2 -2
- package/dist/cohort/index.d.ts.map +1 -0
- package/dist/cohort/index.js +117 -2
- package/dist/cohort/tracker.d.ts +3 -7
- package/dist/cohort/tracker.d.ts.map +1 -1
- package/dist/cohort/tracker.js +106 -87
- package/dist/funnel/analyzer.d.ts +4 -8
- package/dist/funnel/analyzer.d.ts.map +1 -1
- package/dist/funnel/analyzer.js +67 -62
- package/dist/funnel/index.d.ts +2 -2
- package/dist/funnel/index.d.ts.map +1 -0
- package/dist/funnel/index.js +69 -3
- package/dist/growth/hypothesis-generator.d.ts +11 -15
- package/dist/growth/hypothesis-generator.d.ts.map +1 -1
- package/dist/growth/hypothesis-generator.js +46 -39
- package/dist/growth/index.d.ts +2 -2
- package/dist/growth/index.d.ts.map +1 -0
- package/dist/growth/index.js +47 -3
- package/dist/index.d.ts +8 -10
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +724 -12
- package/dist/lifecycle/index.d.ts +4 -4
- package/dist/lifecycle/index.d.ts.map +1 -0
- package/dist/lifecycle/index.js +287 -4
- package/dist/lifecycle/metric-collectors.d.ts +22 -26
- package/dist/lifecycle/metric-collectors.d.ts.map +1 -1
- package/dist/lifecycle/metric-collectors.js +55 -44
- package/dist/lifecycle/posthog-bridge.d.ts +9 -13
- package/dist/lifecycle/posthog-bridge.d.ts.map +1 -1
- package/dist/lifecycle/posthog-bridge.js +77 -25
- package/dist/lifecycle/posthog-metric-source.d.ts +40 -44
- package/dist/lifecycle/posthog-metric-source.d.ts.map +1 -1
- package/dist/lifecycle/posthog-metric-source.js +200 -180
- package/dist/node/churn/index.js +77 -0
- package/dist/node/churn/predictor.js +77 -0
- package/dist/node/cohort/index.js +117 -0
- package/dist/node/cohort/tracker.js +117 -0
- package/dist/node/funnel/analyzer.js +68 -0
- package/dist/node/funnel/index.js +68 -0
- package/dist/node/growth/hypothesis-generator.js +46 -0
- package/dist/node/growth/index.js +46 -0
- package/dist/node/index.js +723 -0
- package/dist/node/lifecycle/index.js +287 -0
- package/dist/node/lifecycle/metric-collectors.js +58 -0
- package/dist/node/lifecycle/posthog-bridge.js +79 -0
- package/dist/node/lifecycle/posthog-metric-source.js +205 -0
- package/dist/node/posthog/event-source.js +138 -0
- package/dist/node/posthog/index.js +138 -0
- package/dist/node/types.js +0 -0
- package/dist/posthog/event-source.d.ts +18 -22
- package/dist/posthog/event-source.d.ts.map +1 -1
- package/dist/posthog/event-source.js +131 -111
- package/dist/posthog/index.d.ts +2 -2
- package/dist/posthog/index.d.ts.map +1 -0
- package/dist/posthog/index.js +139 -3
- package/dist/types.d.ts +52 -55
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/package.json +189 -46
- package/dist/churn/predictor.js.map +0 -1
- package/dist/cohort/tracker.js.map +0 -1
- package/dist/funnel/analyzer.js.map +0 -1
- package/dist/growth/hypothesis-generator.js.map +0 -1
- package/dist/lifecycle/metric-collectors.js.map +0 -1
- package/dist/lifecycle/posthog-bridge.js.map +0 -1
- package/dist/lifecycle/posthog-metric-source.js.map +0 -1
- package/dist/posthog/event-source.js.map +0 -1
package/dist/posthog/index.js
CHANGED
|
@@ -1,3 +1,139 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// @bun
|
|
2
|
+
// src/posthog/event-source.ts
|
|
3
|
+
class PosthogAnalyticsEventSource {
|
|
4
|
+
reader;
|
|
5
|
+
limitPerEvent;
|
|
6
|
+
tenantPropertyKey;
|
|
7
|
+
defaultRangeDays;
|
|
8
|
+
constructor(reader, options = {}) {
|
|
9
|
+
this.reader = reader;
|
|
10
|
+
this.limitPerEvent = options.limitPerEvent ?? 1000;
|
|
11
|
+
this.tenantPropertyKey = options.tenantPropertyKey ?? "tenantId";
|
|
12
|
+
this.defaultRangeDays = options.defaultRangeDays ?? 30;
|
|
13
|
+
}
|
|
14
|
+
async getEventsForFunnel(definition, dateRange) {
|
|
15
|
+
const eventNames = definition.steps.map((step) => step.eventName);
|
|
16
|
+
return this.getEventsByNames(eventNames, dateRange);
|
|
17
|
+
}
|
|
18
|
+
async getEventsForCohort(definition, dateRange, eventNames) {
|
|
19
|
+
const events = eventNames && eventNames.length > 0 ? eventNames : ["*"];
|
|
20
|
+
if (events[0] === "*") {
|
|
21
|
+
return this.getEventsByNames([], dateRange, definition.periods * 500);
|
|
22
|
+
}
|
|
23
|
+
return this.getEventsByNames(events, dateRange, definition.periods * 500);
|
|
24
|
+
}
|
|
25
|
+
async getUserActivity(userId, dateRange, limit = 1000) {
|
|
26
|
+
if (!this.reader.getEvents) {
|
|
27
|
+
throw new Error("Analytics reader does not support event queries.");
|
|
28
|
+
}
|
|
29
|
+
const response = await this.reader.getEvents({
|
|
30
|
+
distinctId: userId,
|
|
31
|
+
dateRange,
|
|
32
|
+
limit
|
|
33
|
+
});
|
|
34
|
+
return response.results.map((event) => toAnalyticsEvent(event, this.tenantPropertyKey));
|
|
35
|
+
}
|
|
36
|
+
async getGrowthMetrics(metricNames, dateRange) {
|
|
37
|
+
const range = resolveRange(dateRange, this.defaultRangeDays);
|
|
38
|
+
const previous = shiftRange(range);
|
|
39
|
+
const results = [];
|
|
40
|
+
for (const metricName of metricNames) {
|
|
41
|
+
const current = await this.countEvents(metricName, range);
|
|
42
|
+
const previousCount = await this.countEvents(metricName, previous);
|
|
43
|
+
results.push({
|
|
44
|
+
name: metricName,
|
|
45
|
+
current,
|
|
46
|
+
previous: previousCount
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
async getEventsByNames(eventNames, dateRange, limitPerEvent = this.limitPerEvent) {
|
|
52
|
+
if (!this.reader.getEvents) {
|
|
53
|
+
throw new Error("Analytics reader does not support event queries.");
|
|
54
|
+
}
|
|
55
|
+
if (eventNames.length === 0) {
|
|
56
|
+
const response = await this.reader.getEvents({
|
|
57
|
+
dateRange,
|
|
58
|
+
limit: limitPerEvent
|
|
59
|
+
});
|
|
60
|
+
return response.results.map((event) => toAnalyticsEvent(event, this.tenantPropertyKey));
|
|
61
|
+
}
|
|
62
|
+
const events = [];
|
|
63
|
+
for (const eventName of eventNames) {
|
|
64
|
+
const response = await this.reader.getEvents({
|
|
65
|
+
event: eventName,
|
|
66
|
+
dateRange,
|
|
67
|
+
limit: limitPerEvent
|
|
68
|
+
});
|
|
69
|
+
response.results.forEach((event) => {
|
|
70
|
+
events.push(toAnalyticsEvent(event, this.tenantPropertyKey));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return events;
|
|
74
|
+
}
|
|
75
|
+
async countEvents(eventName, range) {
|
|
76
|
+
if (!this.reader.queryHogQL) {
|
|
77
|
+
throw new Error("Analytics reader does not support HogQL queries.");
|
|
78
|
+
}
|
|
79
|
+
const result = await this.reader.queryHogQL({
|
|
80
|
+
query: [
|
|
81
|
+
"select",
|
|
82
|
+
" count() as total",
|
|
83
|
+
"from events",
|
|
84
|
+
"where event = {eventName}",
|
|
85
|
+
"and timestamp >= {dateFrom}",
|
|
86
|
+
"and timestamp < {dateTo}"
|
|
87
|
+
].join(`
|
|
88
|
+
`),
|
|
89
|
+
values: {
|
|
90
|
+
eventName,
|
|
91
|
+
dateFrom: range.from.toISOString(),
|
|
92
|
+
dateTo: range.to.toISOString()
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
return readSingleNumber(result) ?? 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function toAnalyticsEvent(event, tenantPropertyKey) {
|
|
99
|
+
const tenantIdValue = event.properties?.[tenantPropertyKey] ?? event.properties?.tenantId;
|
|
100
|
+
return {
|
|
101
|
+
name: event.event,
|
|
102
|
+
userId: event.distinctId,
|
|
103
|
+
tenantId: typeof tenantIdValue === "string" ? tenantIdValue : undefined,
|
|
104
|
+
timestamp: event.timestamp,
|
|
105
|
+
properties: event.properties
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function resolveRange(dateRange, defaultDays) {
|
|
109
|
+
const to = dateRange?.to instanceof Date ? dateRange.to : dateRange?.to ? new Date(dateRange.to) : new Date;
|
|
110
|
+
const from = dateRange?.from instanceof Date ? dateRange.from : dateRange?.from ? new Date(dateRange.from) : new Date(to.getTime() - defaultDays * 24 * 60 * 60 * 1000);
|
|
111
|
+
return { from, to };
|
|
112
|
+
}
|
|
113
|
+
function shiftRange(range) {
|
|
114
|
+
const duration = range.to.getTime() - range.from.getTime();
|
|
115
|
+
return {
|
|
116
|
+
from: new Date(range.from.getTime() - duration),
|
|
117
|
+
to: new Date(range.to.getTime() - duration)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function readSingleNumber(result) {
|
|
121
|
+
if (!Array.isArray(result.results) || result.results.length === 0) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const firstRow = result.results[0];
|
|
125
|
+
if (Array.isArray(firstRow) && firstRow.length > 0) {
|
|
126
|
+
const value = firstRow[0];
|
|
127
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
128
|
+
return value;
|
|
129
|
+
if (typeof value === "string" && value.trim()) {
|
|
130
|
+
const parsed = Number(value);
|
|
131
|
+
if (Number.isFinite(parsed))
|
|
132
|
+
return parsed;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
export {
|
|
138
|
+
PosthogAnalyticsEventSource
|
|
139
|
+
};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,68 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
properties?: Record<string, unknown>;
|
|
1
|
+
export interface AnalyticsEvent {
|
|
2
|
+
name: string;
|
|
3
|
+
userId: string;
|
|
4
|
+
tenantId?: string;
|
|
5
|
+
timestamp: string | Date;
|
|
6
|
+
properties?: Record<string, unknown>;
|
|
8
7
|
}
|
|
9
|
-
interface FunnelStep {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
export interface FunnelStep {
|
|
9
|
+
id: string;
|
|
10
|
+
eventName: string;
|
|
11
|
+
match?: (event: AnalyticsEvent) => boolean;
|
|
13
12
|
}
|
|
14
|
-
interface FunnelDefinition {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
export interface FunnelDefinition {
|
|
14
|
+
name: string;
|
|
15
|
+
steps: FunnelStep[];
|
|
16
|
+
windowHours?: number;
|
|
18
17
|
}
|
|
19
|
-
interface FunnelStepResult {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
export interface FunnelStepResult {
|
|
19
|
+
step: FunnelStep;
|
|
20
|
+
count: number;
|
|
21
|
+
conversionRate: number;
|
|
22
|
+
dropOffRate: number;
|
|
24
23
|
}
|
|
25
|
-
interface FunnelAnalysis {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
export interface FunnelAnalysis {
|
|
25
|
+
definition: FunnelDefinition;
|
|
26
|
+
totalUsers: number;
|
|
27
|
+
steps: FunnelStepResult[];
|
|
29
28
|
}
|
|
30
|
-
interface CohortEvent extends AnalyticsEvent {
|
|
31
|
-
|
|
29
|
+
export interface CohortEvent extends AnalyticsEvent {
|
|
30
|
+
amount?: number;
|
|
32
31
|
}
|
|
33
|
-
interface CohortDefinition {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
export interface CohortDefinition {
|
|
33
|
+
bucket: 'day' | 'week' | 'month';
|
|
34
|
+
periods: number;
|
|
35
|
+
startDate?: Date;
|
|
37
36
|
}
|
|
38
|
-
interface CohortStats {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
export interface CohortStats {
|
|
38
|
+
cohortKey: string;
|
|
39
|
+
users: number;
|
|
40
|
+
retention: number[];
|
|
41
|
+
ltv: number;
|
|
43
42
|
}
|
|
44
|
-
interface CohortAnalysis {
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
export interface CohortAnalysis {
|
|
44
|
+
definition: CohortDefinition;
|
|
45
|
+
cohorts: CohortStats[];
|
|
47
46
|
}
|
|
48
|
-
interface ChurnSignal {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
export interface ChurnSignal {
|
|
48
|
+
userId: string;
|
|
49
|
+
score: number;
|
|
50
|
+
bucket: 'low' | 'medium' | 'high';
|
|
51
|
+
drivers: string[];
|
|
53
52
|
}
|
|
54
|
-
interface GrowthMetric {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
export interface GrowthMetric {
|
|
54
|
+
name: string;
|
|
55
|
+
current: number;
|
|
56
|
+
previous?: number;
|
|
57
|
+
target?: number;
|
|
59
58
|
}
|
|
60
|
-
interface GrowthHypothesis {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
export interface GrowthHypothesis {
|
|
60
|
+
statement: string;
|
|
61
|
+
metric: string;
|
|
62
|
+
confidence: 'low' | 'medium' | 'high';
|
|
63
|
+
impact: 'low' | 'medium' | 'high';
|
|
65
64
|
}
|
|
66
|
-
//#endregion
|
|
67
|
-
export { AnalyticsEvent, ChurnSignal, CohortAnalysis, CohortDefinition, CohortEvent, CohortStats, FunnelAnalysis, FunnelDefinition, FunnelStep, FunnelStepResult, GrowthHypothesis, GrowthMetric };
|
|
68
65
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC;CAC5C;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,gBAAgB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,WAAY,SAAQ,cAAc;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,gBAAgB,CAAC;IAC7B,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAClC,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACtC,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;CACnC"}
|
package/dist/types.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractspec/lib.analytics",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.59.0",
|
|
4
4
|
"description": "Product analytics and growth metrics",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"contractspec",
|
|
@@ -19,66 +19,209 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
|
|
21
21
|
"publish:pkg:canary": "bun publish:pkg --tag canary",
|
|
22
|
-
"build": "bun build:
|
|
23
|
-
"build:bundle": "
|
|
24
|
-
"build:types": "
|
|
25
|
-
"dev": "bun
|
|
22
|
+
"build": "bun run prebuild && bun run build:bundle && bun run build:types",
|
|
23
|
+
"build:bundle": "contractspec-bun-build transpile",
|
|
24
|
+
"build:types": "contractspec-bun-build types",
|
|
25
|
+
"dev": "contractspec-bun-build dev",
|
|
26
26
|
"clean": "rimraf dist .turbo",
|
|
27
27
|
"lint": "bun lint:fix",
|
|
28
28
|
"lint:fix": "eslint src --fix",
|
|
29
29
|
"lint:check": "eslint src",
|
|
30
|
-
"test": "bun test"
|
|
30
|
+
"test": "bun test",
|
|
31
|
+
"prebuild": "contractspec-bun-build prebuild",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
31
33
|
},
|
|
32
34
|
"dependencies": {
|
|
33
|
-
"@contractspec/lib.contracts": "1.
|
|
34
|
-
"@contractspec/lib.lifecycle": "1.
|
|
35
|
+
"@contractspec/lib.contracts": "1.59.0",
|
|
36
|
+
"@contractspec/lib.lifecycle": "1.59.0",
|
|
35
37
|
"dayjs": "^1.11.13"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
|
-
"@contractspec/tool.
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"typescript": "^5.9.3"
|
|
40
|
+
"@contractspec/tool.typescript": "1.59.0",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"@contractspec/tool.bun": "1.58.0"
|
|
42
43
|
},
|
|
43
44
|
"exports": {
|
|
44
|
-
".": "./
|
|
45
|
-
"./churn": "./
|
|
46
|
-
"./churn/
|
|
47
|
-
"./
|
|
48
|
-
"./cohort
|
|
49
|
-
"./
|
|
50
|
-
"./
|
|
51
|
-
"./
|
|
52
|
-
"./
|
|
53
|
-
"./
|
|
54
|
-
"./
|
|
55
|
-
"./
|
|
56
|
-
"./
|
|
57
|
-
"./
|
|
58
|
-
"./
|
|
59
|
-
"./
|
|
60
|
-
"
|
|
45
|
+
".": "./src/index.ts",
|
|
46
|
+
"./churn": "./src/churn/index.ts",
|
|
47
|
+
"./churn/index": "./src/churn/index.ts",
|
|
48
|
+
"./churn/predictor": "./src/churn/predictor.ts",
|
|
49
|
+
"./cohort": "./src/cohort/index.ts",
|
|
50
|
+
"./cohort/index": "./src/cohort/index.ts",
|
|
51
|
+
"./cohort/tracker": "./src/cohort/tracker.ts",
|
|
52
|
+
"./funnel": "./src/funnel/index.ts",
|
|
53
|
+
"./funnel/analyzer": "./src/funnel/analyzer.ts",
|
|
54
|
+
"./funnel/index": "./src/funnel/index.ts",
|
|
55
|
+
"./growth": "./src/growth/index.ts",
|
|
56
|
+
"./growth/hypothesis-generator": "./src/growth/hypothesis-generator.ts",
|
|
57
|
+
"./growth/index": "./src/growth/index.ts",
|
|
58
|
+
"./lifecycle": "./src/lifecycle/index.ts",
|
|
59
|
+
"./lifecycle/index": "./src/lifecycle/index.ts",
|
|
60
|
+
"./lifecycle/metric-collectors": "./src/lifecycle/metric-collectors.ts",
|
|
61
|
+
"./lifecycle/posthog-bridge": "./src/lifecycle/posthog-bridge.ts",
|
|
62
|
+
"./lifecycle/posthog-metric-source": "./src/lifecycle/posthog-metric-source.ts",
|
|
63
|
+
"./posthog": "./src/posthog/index.ts",
|
|
64
|
+
"./posthog/event-source": "./src/posthog/event-source.ts",
|
|
65
|
+
"./posthog/index": "./src/posthog/index.ts",
|
|
66
|
+
"./types": "./src/types.ts"
|
|
61
67
|
},
|
|
62
68
|
"publishConfig": {
|
|
63
69
|
"access": "public",
|
|
64
70
|
"exports": {
|
|
65
|
-
".":
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"./
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"./
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
".": {
|
|
72
|
+
"types": "./dist/index.d.ts",
|
|
73
|
+
"bun": "./dist/index.js",
|
|
74
|
+
"node": "./dist/node/index.mjs",
|
|
75
|
+
"browser": "./dist/browser/index.js",
|
|
76
|
+
"default": "./dist/index.js"
|
|
77
|
+
},
|
|
78
|
+
"./churn": {
|
|
79
|
+
"types": "./dist/churn/index.d.ts",
|
|
80
|
+
"bun": "./dist/churn/index.js",
|
|
81
|
+
"node": "./dist/node/churn/index.mjs",
|
|
82
|
+
"browser": "./dist/browser/churn/index.js",
|
|
83
|
+
"default": "./dist/churn/index.js"
|
|
84
|
+
},
|
|
85
|
+
"./churn/index": {
|
|
86
|
+
"types": "./dist/churn/index.d.ts",
|
|
87
|
+
"bun": "./dist/churn/index.js",
|
|
88
|
+
"node": "./dist/node/churn/index.mjs",
|
|
89
|
+
"browser": "./dist/browser/churn/index.js",
|
|
90
|
+
"default": "./dist/churn/index.js"
|
|
91
|
+
},
|
|
92
|
+
"./churn/predictor": {
|
|
93
|
+
"types": "./dist/churn/predictor.d.ts",
|
|
94
|
+
"bun": "./dist/churn/predictor.js",
|
|
95
|
+
"node": "./dist/node/churn/predictor.mjs",
|
|
96
|
+
"browser": "./dist/browser/churn/predictor.js",
|
|
97
|
+
"default": "./dist/churn/predictor.js"
|
|
98
|
+
},
|
|
99
|
+
"./cohort": {
|
|
100
|
+
"types": "./dist/cohort/index.d.ts",
|
|
101
|
+
"bun": "./dist/cohort/index.js",
|
|
102
|
+
"node": "./dist/node/cohort/index.mjs",
|
|
103
|
+
"browser": "./dist/browser/cohort/index.js",
|
|
104
|
+
"default": "./dist/cohort/index.js"
|
|
105
|
+
},
|
|
106
|
+
"./cohort/index": {
|
|
107
|
+
"types": "./dist/cohort/index.d.ts",
|
|
108
|
+
"bun": "./dist/cohort/index.js",
|
|
109
|
+
"node": "./dist/node/cohort/index.mjs",
|
|
110
|
+
"browser": "./dist/browser/cohort/index.js",
|
|
111
|
+
"default": "./dist/cohort/index.js"
|
|
112
|
+
},
|
|
113
|
+
"./cohort/tracker": {
|
|
114
|
+
"types": "./dist/cohort/tracker.d.ts",
|
|
115
|
+
"bun": "./dist/cohort/tracker.js",
|
|
116
|
+
"node": "./dist/node/cohort/tracker.mjs",
|
|
117
|
+
"browser": "./dist/browser/cohort/tracker.js",
|
|
118
|
+
"default": "./dist/cohort/tracker.js"
|
|
119
|
+
},
|
|
120
|
+
"./funnel": {
|
|
121
|
+
"types": "./dist/funnel/index.d.ts",
|
|
122
|
+
"bun": "./dist/funnel/index.js",
|
|
123
|
+
"node": "./dist/node/funnel/index.mjs",
|
|
124
|
+
"browser": "./dist/browser/funnel/index.js",
|
|
125
|
+
"default": "./dist/funnel/index.js"
|
|
126
|
+
},
|
|
127
|
+
"./funnel/analyzer": {
|
|
128
|
+
"types": "./dist/funnel/analyzer.d.ts",
|
|
129
|
+
"bun": "./dist/funnel/analyzer.js",
|
|
130
|
+
"node": "./dist/node/funnel/analyzer.mjs",
|
|
131
|
+
"browser": "./dist/browser/funnel/analyzer.js",
|
|
132
|
+
"default": "./dist/funnel/analyzer.js"
|
|
133
|
+
},
|
|
134
|
+
"./funnel/index": {
|
|
135
|
+
"types": "./dist/funnel/index.d.ts",
|
|
136
|
+
"bun": "./dist/funnel/index.js",
|
|
137
|
+
"node": "./dist/node/funnel/index.mjs",
|
|
138
|
+
"browser": "./dist/browser/funnel/index.js",
|
|
139
|
+
"default": "./dist/funnel/index.js"
|
|
140
|
+
},
|
|
141
|
+
"./growth": {
|
|
142
|
+
"types": "./dist/growth/index.d.ts",
|
|
143
|
+
"bun": "./dist/growth/index.js",
|
|
144
|
+
"node": "./dist/node/growth/index.mjs",
|
|
145
|
+
"browser": "./dist/browser/growth/index.js",
|
|
146
|
+
"default": "./dist/growth/index.js"
|
|
147
|
+
},
|
|
148
|
+
"./growth/hypothesis-generator": {
|
|
149
|
+
"types": "./dist/growth/hypothesis-generator.d.ts",
|
|
150
|
+
"bun": "./dist/growth/hypothesis-generator.js",
|
|
151
|
+
"node": "./dist/node/growth/hypothesis-generator.mjs",
|
|
152
|
+
"browser": "./dist/browser/growth/hypothesis-generator.js",
|
|
153
|
+
"default": "./dist/growth/hypothesis-generator.js"
|
|
154
|
+
},
|
|
155
|
+
"./growth/index": {
|
|
156
|
+
"types": "./dist/growth/index.d.ts",
|
|
157
|
+
"bun": "./dist/growth/index.js",
|
|
158
|
+
"node": "./dist/node/growth/index.mjs",
|
|
159
|
+
"browser": "./dist/browser/growth/index.js",
|
|
160
|
+
"default": "./dist/growth/index.js"
|
|
161
|
+
},
|
|
162
|
+
"./lifecycle": {
|
|
163
|
+
"types": "./dist/lifecycle/index.d.ts",
|
|
164
|
+
"bun": "./dist/lifecycle/index.js",
|
|
165
|
+
"node": "./dist/node/lifecycle/index.mjs",
|
|
166
|
+
"browser": "./dist/browser/lifecycle/index.js",
|
|
167
|
+
"default": "./dist/lifecycle/index.js"
|
|
168
|
+
},
|
|
169
|
+
"./lifecycle/index": {
|
|
170
|
+
"types": "./dist/lifecycle/index.d.ts",
|
|
171
|
+
"bun": "./dist/lifecycle/index.js",
|
|
172
|
+
"node": "./dist/node/lifecycle/index.mjs",
|
|
173
|
+
"browser": "./dist/browser/lifecycle/index.js",
|
|
174
|
+
"default": "./dist/lifecycle/index.js"
|
|
175
|
+
},
|
|
176
|
+
"./lifecycle/metric-collectors": {
|
|
177
|
+
"types": "./dist/lifecycle/metric-collectors.d.ts",
|
|
178
|
+
"bun": "./dist/lifecycle/metric-collectors.js",
|
|
179
|
+
"node": "./dist/node/lifecycle/metric-collectors.mjs",
|
|
180
|
+
"browser": "./dist/browser/lifecycle/metric-collectors.js",
|
|
181
|
+
"default": "./dist/lifecycle/metric-collectors.js"
|
|
182
|
+
},
|
|
183
|
+
"./lifecycle/posthog-bridge": {
|
|
184
|
+
"types": "./dist/lifecycle/posthog-bridge.d.ts",
|
|
185
|
+
"bun": "./dist/lifecycle/posthog-bridge.js",
|
|
186
|
+
"node": "./dist/node/lifecycle/posthog-bridge.mjs",
|
|
187
|
+
"browser": "./dist/browser/lifecycle/posthog-bridge.js",
|
|
188
|
+
"default": "./dist/lifecycle/posthog-bridge.js"
|
|
189
|
+
},
|
|
190
|
+
"./lifecycle/posthog-metric-source": {
|
|
191
|
+
"types": "./dist/lifecycle/posthog-metric-source.d.ts",
|
|
192
|
+
"bun": "./dist/lifecycle/posthog-metric-source.js",
|
|
193
|
+
"node": "./dist/node/lifecycle/posthog-metric-source.mjs",
|
|
194
|
+
"browser": "./dist/browser/lifecycle/posthog-metric-source.js",
|
|
195
|
+
"default": "./dist/lifecycle/posthog-metric-source.js"
|
|
196
|
+
},
|
|
197
|
+
"./posthog": {
|
|
198
|
+
"types": "./dist/posthog/index.d.ts",
|
|
199
|
+
"bun": "./dist/posthog/index.js",
|
|
200
|
+
"node": "./dist/node/posthog/index.mjs",
|
|
201
|
+
"browser": "./dist/browser/posthog/index.js",
|
|
202
|
+
"default": "./dist/posthog/index.js"
|
|
203
|
+
},
|
|
204
|
+
"./posthog/event-source": {
|
|
205
|
+
"types": "./dist/posthog/event-source.d.ts",
|
|
206
|
+
"bun": "./dist/posthog/event-source.js",
|
|
207
|
+
"node": "./dist/node/posthog/event-source.mjs",
|
|
208
|
+
"browser": "./dist/browser/posthog/event-source.js",
|
|
209
|
+
"default": "./dist/posthog/event-source.js"
|
|
210
|
+
},
|
|
211
|
+
"./posthog/index": {
|
|
212
|
+
"types": "./dist/posthog/index.d.ts",
|
|
213
|
+
"bun": "./dist/posthog/index.js",
|
|
214
|
+
"node": "./dist/node/posthog/index.mjs",
|
|
215
|
+
"browser": "./dist/browser/posthog/index.js",
|
|
216
|
+
"default": "./dist/posthog/index.js"
|
|
217
|
+
},
|
|
218
|
+
"./types": {
|
|
219
|
+
"types": "./dist/types.d.ts",
|
|
220
|
+
"bun": "./dist/types.js",
|
|
221
|
+
"node": "./dist/node/types.mjs",
|
|
222
|
+
"browser": "./dist/browser/types.js",
|
|
223
|
+
"default": "./dist/types.js"
|
|
224
|
+
}
|
|
82
225
|
},
|
|
83
226
|
"registry": "https://registry.npmjs.org/"
|
|
84
227
|
},
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"predictor.js","names":[],"sources":["../../src/churn/predictor.ts"],"sourcesContent":["import dayjs from 'dayjs';\nimport type { AnalyticsEvent, ChurnSignal } from '../types';\n\nexport interface ChurnPredictorOptions {\n recencyWeight?: number;\n frequencyWeight?: number;\n errorWeight?: number;\n decayDays?: number;\n}\n\nexport class ChurnPredictor {\n private readonly recencyWeight: number;\n private readonly frequencyWeight: number;\n private readonly errorWeight: number;\n private readonly decayDays: number;\n\n constructor(options?: ChurnPredictorOptions) {\n this.recencyWeight = options?.recencyWeight ?? 0.5;\n this.frequencyWeight = options?.frequencyWeight ?? 0.3;\n this.errorWeight = options?.errorWeight ?? 0.2;\n this.decayDays = options?.decayDays ?? 14;\n }\n\n score(events: AnalyticsEvent[]): ChurnSignal[] {\n const grouped = groupBy(events, (event) => event.userId);\n const signals: ChurnSignal[] = [];\n for (const [userId, userEvents] of grouped.entries()) {\n const score = this.computeScore(userEvents);\n signals.push({\n userId,\n score,\n bucket: score >= 0.7 ? 'high' : score >= 0.4 ? 'medium' : 'low',\n drivers: this.drivers(userEvents),\n });\n }\n return signals.sort((a, b) => b.score - a.score);\n }\n\n private computeScore(events: AnalyticsEvent[]): number {\n if (!events.length) return 0;\n const sorted = events.sort((a, b) => dateMs(a) - dateMs(b));\n const lastEvent = sorted[sorted.length - 1];\n if (!lastEvent) return 0;\n const daysSinceLast = dayjs().diff(dayjs(lastEvent.timestamp), 'day');\n const recencyScore = Math.max(0, 1 - daysSinceLast / this.decayDays);\n\n const windowStart = dayjs().subtract(this.decayDays, 'day');\n const recentEvents = sorted.filter((event) =>\n dayjs(event.timestamp).isAfter(windowStart)\n );\n const averagePerDay = recentEvents.length / Math.max(this.decayDays, 1);\n const frequencyScore = Math.min(1, averagePerDay * 5);\n\n const errorEvents = recentEvents.filter(\n (event) =>\n typeof event.properties?.error !== 'undefined' ||\n /error|failed/i.test(event.name)\n ).length;\n const errorScore = Math.min(1, errorEvents / 3);\n\n const score =\n recencyScore * this.recencyWeight +\n frequencyScore * this.frequencyWeight +\n (1 - errorScore) * this.errorWeight;\n return Number(score.toFixed(3));\n }\n\n private drivers(events: AnalyticsEvent[]): string[] {\n const drivers: string[] = [];\n const sorted = events.sort((a, b) => dateMs(a) - dateMs(b));\n const lastEvent = sorted[sorted.length - 1];\n if (lastEvent) {\n const days = dayjs().diff(dayjs(lastEvent.timestamp), 'day');\n if (days > this.decayDays) drivers.push(`Inactive for ${days} days`);\n }\n const errorEvents = events.filter(\n (event) =>\n typeof event.properties?.error !== 'undefined' ||\n /error|failed/i.test(event.name)\n );\n if (errorEvents.length) drivers.push(`${errorEvents.length} errors logged`);\n return drivers;\n }\n}\n\nfunction groupBy<T>(\n items: T[],\n selector: (item: T) => string\n): Map<string, T[]> {\n const map = new Map<string, T[]>();\n for (const item of items) {\n const key = selector(item);\n const list = map.get(key) ?? [];\n list.push(item);\n map.set(key, list);\n }\n return map;\n}\n\nfunction dateMs(event: AnalyticsEvent) {\n return new Date(event.timestamp).getTime();\n}\n"],"mappings":";;;AAUA,IAAa,iBAAb,MAA4B;CAC1B,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CAEjB,YAAY,SAAiC;AAC3C,OAAK,gBAAgB,SAAS,iBAAiB;AAC/C,OAAK,kBAAkB,SAAS,mBAAmB;AACnD,OAAK,cAAc,SAAS,eAAe;AAC3C,OAAK,YAAY,SAAS,aAAa;;CAGzC,MAAM,QAAyC;EAC7C,MAAM,UAAU,QAAQ,SAAS,UAAU,MAAM,OAAO;EACxD,MAAM,UAAyB,EAAE;AACjC,OAAK,MAAM,CAAC,QAAQ,eAAe,QAAQ,SAAS,EAAE;GACpD,MAAM,QAAQ,KAAK,aAAa,WAAW;AAC3C,WAAQ,KAAK;IACX;IACA;IACA,QAAQ,SAAS,KAAM,SAAS,SAAS,KAAM,WAAW;IAC1D,SAAS,KAAK,QAAQ,WAAW;IAClC,CAAC;;AAEJ,SAAO,QAAQ,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM;;CAGlD,AAAQ,aAAa,QAAkC;AACrD,MAAI,CAAC,OAAO,OAAQ,QAAO;EAC3B,MAAM,SAAS,OAAO,MAAM,GAAG,MAAM,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC;EAC3D,MAAM,YAAY,OAAO,OAAO,SAAS;AACzC,MAAI,CAAC,UAAW,QAAO;EACvB,MAAM,gBAAgB,OAAO,CAAC,KAAK,MAAM,UAAU,UAAU,EAAE,MAAM;EACrE,MAAM,eAAe,KAAK,IAAI,GAAG,IAAI,gBAAgB,KAAK,UAAU;EAEpE,MAAM,cAAc,OAAO,CAAC,SAAS,KAAK,WAAW,MAAM;EAC3D,MAAM,eAAe,OAAO,QAAQ,UAClC,MAAM,MAAM,UAAU,CAAC,QAAQ,YAAY,CAC5C;EACD,MAAM,gBAAgB,aAAa,SAAS,KAAK,IAAI,KAAK,WAAW,EAAE;EACvE,MAAM,iBAAiB,KAAK,IAAI,GAAG,gBAAgB,EAAE;EAErD,MAAM,cAAc,aAAa,QAC9B,UACC,OAAO,MAAM,YAAY,UAAU,eACnC,gBAAgB,KAAK,MAAM,KAAK,CACnC,CAAC;EACF,MAAM,aAAa,KAAK,IAAI,GAAG,cAAc,EAAE;EAE/C,MAAM,QACJ,eAAe,KAAK,gBACpB,iBAAiB,KAAK,mBACrB,IAAI,cAAc,KAAK;AAC1B,SAAO,OAAO,MAAM,QAAQ,EAAE,CAAC;;CAGjC,AAAQ,QAAQ,QAAoC;EAClD,MAAM,UAAoB,EAAE;EAC5B,MAAM,SAAS,OAAO,MAAM,GAAG,MAAM,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC;EAC3D,MAAM,YAAY,OAAO,OAAO,SAAS;AACzC,MAAI,WAAW;GACb,MAAM,OAAO,OAAO,CAAC,KAAK,MAAM,UAAU,UAAU,EAAE,MAAM;AAC5D,OAAI,OAAO,KAAK,UAAW,SAAQ,KAAK,gBAAgB,KAAK,OAAO;;EAEtE,MAAM,cAAc,OAAO,QACxB,UACC,OAAO,MAAM,YAAY,UAAU,eACnC,gBAAgB,KAAK,MAAM,KAAK,CACnC;AACD,MAAI,YAAY,OAAQ,SAAQ,KAAK,GAAG,YAAY,OAAO,gBAAgB;AAC3E,SAAO;;;AAIX,SAAS,QACP,OACA,UACkB;CAClB,MAAM,sBAAM,IAAI,KAAkB;AAClC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,MAAM,SAAS,KAAK;EAC1B,MAAM,OAAO,IAAI,IAAI,IAAI,IAAI,EAAE;AAC/B,OAAK,KAAK,KAAK;AACf,MAAI,IAAI,KAAK,KAAK;;AAEpB,QAAO;;AAGT,SAAS,OAAO,OAAuB;AACrC,QAAO,IAAI,KAAK,MAAM,UAAU,CAAC,SAAS"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tracker.js","names":[],"sources":["../../src/cohort/tracker.ts"],"sourcesContent":["import dayjs from 'dayjs';\nimport type {\n AnalyticsEvent,\n CohortAnalysis,\n CohortDefinition,\n CohortStats,\n} from '../types';\n\nexport class CohortTracker {\n analyze(\n events: AnalyticsEvent[],\n definition: CohortDefinition\n ): CohortAnalysis {\n const groupedByUser = groupBy(events, (event) => event.userId);\n const cohorts = new Map<string, CohortStatsBuilder>();\n\n for (const [userId, userEvents] of groupedByUser.entries()) {\n userEvents.sort((a, b) => dateMs(a) - dateMs(b));\n const signup = userEvents[0];\n if (!signup) continue;\n const cohortKey = bucketKey(signup.timestamp, definition.bucket);\n const builder =\n cohorts.get(cohortKey) ?? new CohortStatsBuilder(cohortKey, definition);\n builder.addUser(userId);\n for (const event of userEvents) {\n builder.addEvent(userId, event);\n }\n cohorts.set(cohortKey, builder);\n }\n\n return {\n definition,\n cohorts: [...cohorts.values()].map((builder) => builder.build()),\n };\n }\n}\n\nclass CohortStatsBuilder {\n private readonly users = new Set<string>();\n private readonly retentionMap = new Map<number, Set<string>>();\n private ltv = 0;\n constructor(\n private readonly key: string,\n private readonly definition: CohortDefinition\n ) {}\n\n addUser(userId: string) {\n this.users.add(userId);\n }\n\n addEvent(userId: string, event: AnalyticsEvent) {\n const period = bucketDiff(\n this.key,\n event.timestamp,\n this.definition.bucket\n );\n if (period < 0 || period >= this.definition.periods) return;\n const bucket = this.retentionMap.get(period) ?? new Set<string>();\n bucket.add(userId);\n this.retentionMap.set(period, bucket);\n const amount =\n typeof event.properties?.amount === 'number'\n ? event.properties.amount\n : 0;\n this.ltv += amount;\n }\n\n build(): CohortStats {\n const totalUsers = this.users.size || 1;\n const retention: number[] = [];\n for (let period = 0; period < this.definition.periods; period++) {\n const active = this.retentionMap.get(period)?.size ?? 0;\n retention.push(Number((active / totalUsers).toFixed(3)));\n }\n return {\n cohortKey: this.key,\n users: this.users.size,\n retention,\n ltv: Number(this.ltv.toFixed(2)),\n };\n }\n}\n\nfunction groupBy<T>(\n items: T[],\n selector: (item: T) => string\n): Map<string, T[]> {\n const map = new Map<string, T[]>();\n for (const item of items) {\n const key = selector(item);\n const list = map.get(key) ?? [];\n list.push(item);\n map.set(key, list);\n }\n return map;\n}\n\nfunction bucketKey(\n timestamp: string | Date,\n bucket: CohortDefinition['bucket']\n): string {\n const dt = dayjs(timestamp);\n switch (bucket) {\n case 'day':\n return dt.startOf('day').format('YYYY-MM-DD');\n case 'week':\n return dt.startOf('week').format('YYYY-[W]WW');\n case 'month':\n default:\n return dt.startOf('month').format('YYYY-MM');\n }\n}\n\nfunction bucketDiff(\n cohortKey: string,\n timestamp: string | Date,\n bucket: CohortDefinition['bucket']\n): number {\n const start = parseBucketKey(cohortKey, bucket);\n const target = dayjs(timestamp);\n switch (bucket) {\n case 'day':\n return target.diff(start, 'day');\n case 'week':\n return target.diff(start, 'week');\n case 'month':\n default:\n return target.diff(start, 'month');\n }\n}\n\nfunction parseBucketKey(key: string, bucket: CohortDefinition['bucket']) {\n switch (bucket) {\n case 'day':\n return dayjs(key, 'YYYY-MM-DD');\n case 'week':\n return dayjs(key.replace('W', ''), 'YYYY-ww');\n case 'month':\n default:\n return dayjs(key, 'YYYY-MM');\n }\n}\n\nfunction dateMs(event: AnalyticsEvent) {\n return new Date(event.timestamp).getTime();\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.js","names":[],"sources":["../../src/funnel/analyzer.ts"],"sourcesContent":["import type {\n AnalyticsEvent,\n FunnelAnalysis,\n FunnelDefinition,\n FunnelStep,\n FunnelStepResult,\n} from '../types';\n\nexport class FunnelAnalyzer {\n analyze(\n events: AnalyticsEvent[],\n definition: FunnelDefinition\n ): FunnelAnalysis {\n const windowMs = (definition.windowHours ?? 72) * 60 * 60 * 1000;\n const eventsByUser = groupByUser(events);\n const stepCounts = definition.steps.map(() => 0);\n\n for (const userEvents of eventsByUser.values()) {\n const completionIndex = this.evaluateUser(\n userEvents,\n definition.steps,\n windowMs\n );\n completionIndex.forEach((hit, stepIdx) => {\n if (hit) {\n stepCounts[stepIdx] = (stepCounts[stepIdx] ?? 0) + 1;\n }\n });\n }\n\n const totalUsers = eventsByUser.size;\n const steps: FunnelStepResult[] = definition.steps.map((step, index) => {\n const prevCount = index === 0 ? totalUsers : stepCounts[index - 1] || 1;\n const count = stepCounts[index] ?? 0;\n const conversionRate =\n prevCount === 0 ? 0 : Number((count / prevCount).toFixed(3));\n const dropOffRate = Number((1 - conversionRate).toFixed(3));\n return { step, count, conversionRate, dropOffRate };\n });\n\n return {\n definition,\n totalUsers,\n steps,\n };\n }\n\n private evaluateUser(\n events: AnalyticsEvent[],\n steps: FunnelStep[],\n windowMs: number\n ) {\n const sorted = [...events].sort(\n (a, b) =>\n new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()\n );\n const completion: boolean[] = Array(steps.length).fill(false);\n let cursor = 0;\n let anchorTime: number | undefined;\n\n for (const event of sorted) {\n const step = steps[cursor];\n if (!step) break;\n if (event.name !== step.eventName) continue;\n if (step.match && !step.match(event)) continue;\n\n const eventTime = new Date(event.timestamp).getTime();\n if (cursor === 0) {\n anchorTime = eventTime;\n completion[cursor] = true;\n cursor += 1;\n continue;\n }\n\n if (anchorTime && eventTime - anchorTime <= windowMs) {\n completion[cursor] = true;\n cursor += 1;\n }\n }\n\n return completion;\n }\n}\n\nfunction groupByUser(events: AnalyticsEvent[]): Map<string, AnalyticsEvent[]> {\n const map = new Map<string, AnalyticsEvent[]>();\n for (const event of events) {\n const list = map.get(event.userId) ?? [];\n list.push(event);\n map.set(event.userId, list);\n }\n return map;\n}\n"],"mappings":";AAQA,IAAa,iBAAb,MAA4B;CAC1B,QACE,QACA,YACgB;EAChB,MAAM,YAAY,WAAW,eAAe,MAAM,KAAK,KAAK;EAC5D,MAAM,eAAe,YAAY,OAAO;EACxC,MAAM,aAAa,WAAW,MAAM,UAAU,EAAE;AAEhD,OAAK,MAAM,cAAc,aAAa,QAAQ,CAM5C,CALwB,KAAK,aAC3B,YACA,WAAW,OACX,SACD,CACe,SAAS,KAAK,YAAY;AACxC,OAAI,IACF,YAAW,YAAY,WAAW,YAAY,KAAK;IAErD;EAGJ,MAAM,aAAa,aAAa;AAUhC,SAAO;GACL;GACA;GACA,OAZgC,WAAW,MAAM,KAAK,MAAM,UAAU;IACtE,MAAM,YAAY,UAAU,IAAI,aAAa,WAAW,QAAQ,MAAM;IACtE,MAAM,QAAQ,WAAW,UAAU;IACnC,MAAM,iBACJ,cAAc,IAAI,IAAI,QAAQ,QAAQ,WAAW,QAAQ,EAAE,CAAC;AAE9D,WAAO;KAAE;KAAM;KAAO;KAAgB,aADlB,QAAQ,IAAI,gBAAgB,QAAQ,EAAE,CAAC;KACR;KACnD;GAMD;;CAGH,AAAQ,aACN,QACA,OACA,UACA;EACA,MAAM,SAAS,CAAC,GAAG,OAAO,CAAC,MACxB,GAAG,MACF,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,GAAG,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,CACpE;EACD,MAAM,aAAwB,MAAM,MAAM,OAAO,CAAC,KAAK,MAAM;EAC7D,IAAI,SAAS;EACb,IAAI;AAEJ,OAAK,MAAM,SAAS,QAAQ;GAC1B,MAAM,OAAO,MAAM;AACnB,OAAI,CAAC,KAAM;AACX,OAAI,MAAM,SAAS,KAAK,UAAW;AACnC,OAAI,KAAK,SAAS,CAAC,KAAK,MAAM,MAAM,CAAE;GAEtC,MAAM,YAAY,IAAI,KAAK,MAAM,UAAU,CAAC,SAAS;AACrD,OAAI,WAAW,GAAG;AAChB,iBAAa;AACb,eAAW,UAAU;AACrB,cAAU;AACV;;AAGF,OAAI,cAAc,YAAY,cAAc,UAAU;AACpD,eAAW,UAAU;AACrB,cAAU;;;AAId,SAAO;;;AAIX,SAAS,YAAY,QAAyD;CAC5E,MAAM,sBAAM,IAAI,KAA+B;AAC/C,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,OAAO,IAAI,IAAI,MAAM,OAAO,IAAI,EAAE;AACxC,OAAK,KAAK,MAAM;AAChB,MAAI,IAAI,MAAM,QAAQ,KAAK;;AAE7B,QAAO"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hypothesis-generator.js","names":[],"sources":["../../src/growth/hypothesis-generator.ts"],"sourcesContent":["import type { GrowthHypothesis, GrowthMetric } from '../types';\n\nexport interface HypothesisGeneratorOptions {\n minDelta?: number;\n}\n\nexport class GrowthHypothesisGenerator {\n private readonly minDelta: number;\n\n constructor(options?: HypothesisGeneratorOptions) {\n this.minDelta = options?.minDelta ?? 0.05;\n }\n\n generate(metrics: GrowthMetric[]): GrowthHypothesis[] {\n return metrics\n .map((metric) => this.fromMetric(metric))\n .filter((hypothesis): hypothesis is GrowthHypothesis =>\n Boolean(hypothesis)\n );\n }\n\n private fromMetric(metric: GrowthMetric): GrowthHypothesis | null {\n const change = this.delta(metric);\n if (Math.abs(change) < this.minDelta) return null;\n const direction = change > 0 ? 'rising' : 'declining';\n const statement = this.statement(metric, change, direction);\n return {\n statement,\n metric: metric.name,\n confidence: Math.abs(change) > 0.2 ? 'high' : 'medium',\n impact: this.impact(metric),\n };\n }\n\n private delta(metric: GrowthMetric): number {\n if (metric.previous == null) return 0;\n const prev = metric.previous || 1;\n return (metric.current - prev) / Math.abs(prev);\n }\n\n private impact(metric: GrowthMetric): GrowthHypothesis['impact'] {\n if (metric.target && metric.current < metric.target * 0.8) return 'high';\n if (metric.target && metric.current < metric.target) return 'medium';\n return 'low';\n }\n\n private statement(\n metric: GrowthMetric,\n change: number,\n direction: string\n ): string {\n const percent = Math.abs(parseFloat((change * 100).toFixed(1)));\n if (direction === 'declining') {\n return `${metric.name} is down ${percent}% vs last period; test new onboarding prompts to recover activation.`;\n }\n return `${metric.name} grew ${percent}% period-over-period; double down with expanded experiment or pricing test.`;\n }\n}\n"],"mappings":";AAMA,IAAa,4BAAb,MAAuC;CACrC,AAAiB;CAEjB,YAAY,SAAsC;AAChD,OAAK,WAAW,SAAS,YAAY;;CAGvC,SAAS,SAA6C;AACpD,SAAO,QACJ,KAAK,WAAW,KAAK,WAAW,OAAO,CAAC,CACxC,QAAQ,eACP,QAAQ,WAAW,CACpB;;CAGL,AAAQ,WAAW,QAA+C;EAChE,MAAM,SAAS,KAAK,MAAM,OAAO;AACjC,MAAI,KAAK,IAAI,OAAO,GAAG,KAAK,SAAU,QAAO;EAC7C,MAAM,YAAY,SAAS,IAAI,WAAW;AAE1C,SAAO;GACL,WAFgB,KAAK,UAAU,QAAQ,QAAQ,UAAU;GAGzD,QAAQ,OAAO;GACf,YAAY,KAAK,IAAI,OAAO,GAAG,KAAM,SAAS;GAC9C,QAAQ,KAAK,OAAO,OAAO;GAC5B;;CAGH,AAAQ,MAAM,QAA8B;AAC1C,MAAI,OAAO,YAAY,KAAM,QAAO;EACpC,MAAM,OAAO,OAAO,YAAY;AAChC,UAAQ,OAAO,UAAU,QAAQ,KAAK,IAAI,KAAK;;CAGjD,AAAQ,OAAO,QAAkD;AAC/D,MAAI,OAAO,UAAU,OAAO,UAAU,OAAO,SAAS,GAAK,QAAO;AAClE,MAAI,OAAO,UAAU,OAAO,UAAU,OAAO,OAAQ,QAAO;AAC5D,SAAO;;CAGT,AAAQ,UACN,QACA,QACA,WACQ;EACR,MAAM,UAAU,KAAK,IAAI,YAAY,SAAS,KAAK,QAAQ,EAAE,CAAC,CAAC;AAC/D,MAAI,cAAc,YAChB,QAAO,GAAG,OAAO,KAAK,WAAW,QAAQ;AAE3C,SAAO,GAAG,OAAO,KAAK,QAAQ,QAAQ"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"metric-collectors.js","names":[],"sources":["../../src/lifecycle/metric-collectors.ts"],"sourcesContent":["import type {\n LifecycleMetricSnapshot,\n LifecycleSignal,\n LifecycleStage,\n} from '@contractspec/lib.lifecycle';\nimport type { AnalyticsEvent } from '../types';\n\nexport interface LifecycleMetricSource {\n getActiveUsers(): Promise<number | undefined>;\n getWeeklyActiveUsers?(): Promise<number | undefined>;\n getRetentionRate?(): Promise<number | undefined>;\n getMonthlyRecurringRevenue?(): Promise<number | undefined>;\n getCustomerCount?(): Promise<number | undefined>;\n getTeamSize?(): Promise<number | undefined>;\n getBurnMultiple?(): Promise<number | undefined>;\n}\n\nexport const collectLifecycleMetrics = async (\n source: LifecycleMetricSource\n): Promise<LifecycleMetricSnapshot> => {\n const [\n activeUsers,\n weeklyActiveUsers,\n retentionRate,\n monthlyRecurringRevenue,\n customerCount,\n teamSize,\n burnMultiple,\n ] = await Promise.all([\n source.getActiveUsers(),\n source.getWeeklyActiveUsers?.(),\n source.getRetentionRate?.(),\n source.getMonthlyRecurringRevenue?.(),\n source.getCustomerCount?.(),\n source.getTeamSize?.(),\n source.getBurnMultiple?.(),\n ]);\n\n return {\n activeUsers,\n weeklyActiveUsers,\n retentionRate,\n monthlyRecurringRevenue,\n customerCount,\n teamSize,\n burnMultiple,\n };\n};\n\nexport const metricsToSignals = (\n metrics: LifecycleMetricSnapshot,\n tenantId?: string\n): LifecycleSignal[] =>\n Object.entries(metrics)\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([metricKey, value]) => ({\n id: `lifecycle-metric:${metricKey}`,\n kind: 'metric',\n source: 'analytics',\n name: metricKey,\n value,\n weight: 1,\n confidence: 0.8,\n details: tenantId ? { tenantId } : undefined,\n capturedAt: new Date().toISOString(),\n }));\n\nexport const lifecycleEventNames = {\n assessmentRun: 'lifecycle_assessment_run',\n stageChanged: 'lifecycle_stage_changed',\n guidanceConsumed: 'lifecycle_guidance_consumed',\n} as const;\n\nexport interface LifecycleStageChangePayload {\n tenantId?: string;\n previousStage?: LifecycleStage;\n nextStage: LifecycleStage;\n confidence: number;\n}\n\nexport const createStageChangeEvent = (\n payload: LifecycleStageChangePayload\n): AnalyticsEvent => ({\n name: lifecycleEventNames.stageChanged,\n userId: 'system',\n tenantId: payload.tenantId,\n timestamp: new Date(),\n properties: { ...payload },\n});\n"],"mappings":";AAiBA,MAAa,0BAA0B,OACrC,WACqC;CACrC,MAAM,CACJ,aACA,mBACA,eACA,yBACA,eACA,UACA,gBACE,MAAM,QAAQ,IAAI;EACpB,OAAO,gBAAgB;EACvB,OAAO,wBAAwB;EAC/B,OAAO,oBAAoB;EAC3B,OAAO,8BAA8B;EACrC,OAAO,oBAAoB;EAC3B,OAAO,eAAe;EACtB,OAAO,mBAAmB;EAC3B,CAAC;AAEF,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACD;;AAGH,MAAa,oBACX,SACA,aAEA,OAAO,QAAQ,QAAQ,CACpB,QAAQ,GAAG,WAAW,UAAU,UAAa,UAAU,KAAK,CAC5D,KAAK,CAAC,WAAW,YAAY;CAC5B,IAAI,oBAAoB;CACxB,MAAM;CACN,QAAQ;CACR,MAAM;CACN;CACA,QAAQ;CACR,YAAY;CACZ,SAAS,WAAW,EAAE,UAAU,GAAG;CACnC,6BAAY,IAAI,MAAM,EAAC,aAAa;CACrC,EAAE;AAEP,MAAa,sBAAsB;CACjC,eAAe;CACf,cAAc;CACd,kBAAkB;CACnB;AASD,MAAa,0BACX,aACoB;CACpB,MAAM,oBAAoB;CAC1B,QAAQ;CACR,UAAU,QAAQ;CAClB,2BAAW,IAAI,MAAM;CACrB,YAAY,EAAE,GAAG,SAAS;CAC3B"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"posthog-bridge.js","names":[],"sources":["../../src/lifecycle/posthog-bridge.ts"],"sourcesContent":["import type { LifecycleAssessment } from '@contractspec/lib.lifecycle';\nimport { lifecycleEventNames } from './metric-collectors';\n\nexport interface PostHogLikeClient {\n capture: (event: {\n distinctId: string;\n event: string;\n properties?: Record<string, unknown>;\n }) => Promise<void> | void;\n}\n\nexport const trackLifecycleAssessment = async (\n client: PostHogLikeClient,\n tenantId: string,\n assessment: LifecycleAssessment\n) => {\n await client.capture({\n distinctId: tenantId,\n event: lifecycleEventNames.assessmentRun,\n properties: {\n stage: assessment.stage,\n confidence: assessment.confidence,\n axes: assessment.axes,\n },\n });\n};\n\nexport const trackLifecycleStageChange = async (\n client: PostHogLikeClient,\n tenantId: string,\n previousStage: number | undefined,\n nextStage: number\n) => {\n await client.capture({\n distinctId: tenantId,\n event: lifecycleEventNames.stageChanged,\n properties: {\n previousStage,\n nextStage,\n },\n });\n};\n"],"mappings":";;;AAWA,MAAa,2BAA2B,OACtC,QACA,UACA,eACG;AACH,OAAM,OAAO,QAAQ;EACnB,YAAY;EACZ,OAAO,oBAAoB;EAC3B,YAAY;GACV,OAAO,WAAW;GAClB,YAAY,WAAW;GACvB,MAAM,WAAW;GAClB;EACF,CAAC;;AAGJ,MAAa,4BAA4B,OACvC,QACA,UACA,eACA,cACG;AACH,OAAM,OAAO,QAAQ;EACnB,YAAY;EACZ,OAAO,oBAAoB;EAC3B,YAAY;GACV;GACA;GACD;EACF,CAAC"}
|