@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
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// src/lifecycle/metric-collectors.ts
|
|
2
|
+
var collectLifecycleMetrics = async (source) => {
|
|
3
|
+
const [
|
|
4
|
+
activeUsers,
|
|
5
|
+
weeklyActiveUsers,
|
|
6
|
+
retentionRate,
|
|
7
|
+
monthlyRecurringRevenue,
|
|
8
|
+
customerCount,
|
|
9
|
+
teamSize,
|
|
10
|
+
burnMultiple
|
|
11
|
+
] = await Promise.all([
|
|
12
|
+
source.getActiveUsers(),
|
|
13
|
+
source.getWeeklyActiveUsers?.(),
|
|
14
|
+
source.getRetentionRate?.(),
|
|
15
|
+
source.getMonthlyRecurringRevenue?.(),
|
|
16
|
+
source.getCustomerCount?.(),
|
|
17
|
+
source.getTeamSize?.(),
|
|
18
|
+
source.getBurnMultiple?.()
|
|
19
|
+
]);
|
|
20
|
+
return {
|
|
21
|
+
activeUsers,
|
|
22
|
+
weeklyActiveUsers,
|
|
23
|
+
retentionRate,
|
|
24
|
+
monthlyRecurringRevenue,
|
|
25
|
+
customerCount,
|
|
26
|
+
teamSize,
|
|
27
|
+
burnMultiple
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
var metricsToSignals = (metrics, tenantId) => Object.entries(metrics).filter(([, value]) => value !== undefined && value !== null).map(([metricKey, value]) => ({
|
|
31
|
+
id: `lifecycle-metric:${metricKey}`,
|
|
32
|
+
kind: "metric",
|
|
33
|
+
source: "analytics",
|
|
34
|
+
name: metricKey,
|
|
35
|
+
value,
|
|
36
|
+
weight: 1,
|
|
37
|
+
confidence: 0.8,
|
|
38
|
+
details: tenantId ? { tenantId } : undefined,
|
|
39
|
+
capturedAt: new Date().toISOString()
|
|
40
|
+
}));
|
|
41
|
+
var lifecycleEventNames = {
|
|
42
|
+
assessmentRun: "lifecycle_assessment_run",
|
|
43
|
+
stageChanged: "lifecycle_stage_changed",
|
|
44
|
+
guidanceConsumed: "lifecycle_guidance_consumed"
|
|
45
|
+
};
|
|
46
|
+
var createStageChangeEvent = (payload) => ({
|
|
47
|
+
name: lifecycleEventNames.stageChanged,
|
|
48
|
+
userId: "system",
|
|
49
|
+
tenantId: payload.tenantId,
|
|
50
|
+
timestamp: new Date,
|
|
51
|
+
properties: { ...payload }
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// src/lifecycle/posthog-bridge.ts
|
|
55
|
+
var trackLifecycleAssessment = async (client, tenantId, assessment) => {
|
|
56
|
+
await client.capture({
|
|
57
|
+
distinctId: tenantId,
|
|
58
|
+
event: lifecycleEventNames.assessmentRun,
|
|
59
|
+
properties: {
|
|
60
|
+
stage: assessment.stage,
|
|
61
|
+
confidence: assessment.confidence,
|
|
62
|
+
axes: assessment.axes
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
var trackLifecycleStageChange = async (client, tenantId, previousStage, nextStage) => {
|
|
67
|
+
await client.capture({
|
|
68
|
+
distinctId: tenantId,
|
|
69
|
+
event: lifecycleEventNames.stageChanged,
|
|
70
|
+
properties: {
|
|
71
|
+
previousStage,
|
|
72
|
+
nextStage
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/lifecycle/posthog-metric-source.ts
|
|
78
|
+
class PosthogLifecycleMetricSource {
|
|
79
|
+
reader;
|
|
80
|
+
activityEvents;
|
|
81
|
+
retentionWindowDays;
|
|
82
|
+
revenueEvent;
|
|
83
|
+
revenueProperty;
|
|
84
|
+
customerEvent;
|
|
85
|
+
customerProperty;
|
|
86
|
+
teamSizeEvent;
|
|
87
|
+
teamSizeProperty;
|
|
88
|
+
burnMultipleEvent;
|
|
89
|
+
burnMultipleProperty;
|
|
90
|
+
constructor(reader, options = {}) {
|
|
91
|
+
this.reader = reader;
|
|
92
|
+
this.activityEvents = options.activityEvents;
|
|
93
|
+
this.retentionWindowDays = options.retentionWindowDays ?? 7;
|
|
94
|
+
this.revenueEvent = options.revenueEvent;
|
|
95
|
+
this.revenueProperty = options.revenueProperty ?? "amount";
|
|
96
|
+
this.customerEvent = options.customerEvent;
|
|
97
|
+
this.customerProperty = options.customerProperty ?? "is_customer";
|
|
98
|
+
this.teamSizeEvent = options.teamSizeEvent;
|
|
99
|
+
this.teamSizeProperty = options.teamSizeProperty ?? "team_size";
|
|
100
|
+
this.burnMultipleEvent = options.burnMultipleEvent;
|
|
101
|
+
this.burnMultipleProperty = options.burnMultipleProperty ?? "burn_multiple";
|
|
102
|
+
}
|
|
103
|
+
async getActiveUsers() {
|
|
104
|
+
return this.countDistinctUsers(1);
|
|
105
|
+
}
|
|
106
|
+
async getWeeklyActiveUsers() {
|
|
107
|
+
return this.countDistinctUsers(7);
|
|
108
|
+
}
|
|
109
|
+
async getRetentionRate() {
|
|
110
|
+
const windowDays = this.retentionWindowDays;
|
|
111
|
+
const now = new Date;
|
|
112
|
+
const prevEnd = new Date(now.getTime() - windowDays * 24 * 60 * 60 * 1000);
|
|
113
|
+
const prevStart = new Date(now.getTime() - windowDays * 2 * 24 * 60 * 60 * 1000);
|
|
114
|
+
const returningUsers = await this.countReturningUsers(prevStart, prevEnd, now);
|
|
115
|
+
const prevUsers = await this.countDistinctUsersBetween(prevStart, prevEnd);
|
|
116
|
+
if (prevUsers === undefined || prevUsers === 0)
|
|
117
|
+
return;
|
|
118
|
+
return returningUsers / prevUsers;
|
|
119
|
+
}
|
|
120
|
+
async getMonthlyRecurringRevenue() {
|
|
121
|
+
if (!this.revenueEvent || !this.revenueProperty)
|
|
122
|
+
return;
|
|
123
|
+
return this.sumMetric(this.revenueEvent, this.revenueProperty);
|
|
124
|
+
}
|
|
125
|
+
async getCustomerCount() {
|
|
126
|
+
if (!this.customerEvent || !this.customerProperty)
|
|
127
|
+
return;
|
|
128
|
+
return this.countDistinctUsersByProperty(this.customerEvent, this.customerProperty);
|
|
129
|
+
}
|
|
130
|
+
async getTeamSize() {
|
|
131
|
+
if (!this.teamSizeEvent || !this.teamSizeProperty)
|
|
132
|
+
return;
|
|
133
|
+
return this.latestMetric(this.teamSizeEvent, this.teamSizeProperty);
|
|
134
|
+
}
|
|
135
|
+
async getBurnMultiple() {
|
|
136
|
+
if (!this.burnMultipleEvent || !this.burnMultipleProperty)
|
|
137
|
+
return;
|
|
138
|
+
return this.latestMetric(this.burnMultipleEvent, this.burnMultipleProperty);
|
|
139
|
+
}
|
|
140
|
+
async countDistinctUsers(days) {
|
|
141
|
+
const range = buildDateRange(days);
|
|
142
|
+
return this.countDistinctUsersBetween(range.from, range.to);
|
|
143
|
+
}
|
|
144
|
+
async countDistinctUsersBetween(from, to) {
|
|
145
|
+
const eventFilter = buildEventFilter(this.activityEvents, "activityEvent");
|
|
146
|
+
const result = await this.queryHogQL({
|
|
147
|
+
query: [
|
|
148
|
+
"select",
|
|
149
|
+
" countDistinct(distinct_id) as total",
|
|
150
|
+
"from events",
|
|
151
|
+
`where timestamp >= {dateFrom} and timestamp < {dateTo}`,
|
|
152
|
+
eventFilter.clause ? `and ${eventFilter.clause}` : ""
|
|
153
|
+
].filter(Boolean).join(`
|
|
154
|
+
`),
|
|
155
|
+
values: {
|
|
156
|
+
dateFrom: from.toISOString(),
|
|
157
|
+
dateTo: to.toISOString(),
|
|
158
|
+
...eventFilter.values
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return readSingleNumber(result);
|
|
162
|
+
}
|
|
163
|
+
async countReturningUsers(previousStart, previousEnd, currentEnd) {
|
|
164
|
+
const eventFilter = buildEventFilter(this.activityEvents, "activityEvent");
|
|
165
|
+
const result = await this.queryHogQL({
|
|
166
|
+
query: [
|
|
167
|
+
"select",
|
|
168
|
+
" countDistinct(distinct_id) as total",
|
|
169
|
+
"from events",
|
|
170
|
+
`where timestamp >= {currentFrom} and timestamp < {currentTo}`,
|
|
171
|
+
eventFilter.clause ? `and ${eventFilter.clause}` : "",
|
|
172
|
+
"and distinct_id in (",
|
|
173
|
+
" select distinct_id from events",
|
|
174
|
+
" where timestamp >= {previousFrom} and timestamp < {previousTo}",
|
|
175
|
+
eventFilter.clause ? `and ${eventFilter.clause}` : "",
|
|
176
|
+
")"
|
|
177
|
+
].join(`
|
|
178
|
+
`),
|
|
179
|
+
values: {
|
|
180
|
+
currentFrom: previousEnd.toISOString(),
|
|
181
|
+
currentTo: currentEnd.toISOString(),
|
|
182
|
+
previousFrom: previousStart.toISOString(),
|
|
183
|
+
previousTo: previousEnd.toISOString(),
|
|
184
|
+
...eventFilter.values
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return readSingleNumber(result) ?? 0;
|
|
188
|
+
}
|
|
189
|
+
async sumMetric(eventName, propertyKey) {
|
|
190
|
+
const result = await this.queryHogQL({
|
|
191
|
+
query: [
|
|
192
|
+
"select",
|
|
193
|
+
` sum(properties.${propertyKey}) as total`,
|
|
194
|
+
"from events",
|
|
195
|
+
"where event = {eventName}"
|
|
196
|
+
].join(`
|
|
197
|
+
`),
|
|
198
|
+
values: { eventName }
|
|
199
|
+
});
|
|
200
|
+
return readSingleNumber(result);
|
|
201
|
+
}
|
|
202
|
+
async countDistinctUsersByProperty(eventName, propertyKey) {
|
|
203
|
+
const result = await this.queryHogQL({
|
|
204
|
+
query: [
|
|
205
|
+
"select",
|
|
206
|
+
" countDistinct(distinct_id) as total",
|
|
207
|
+
"from events",
|
|
208
|
+
"where event = {eventName}",
|
|
209
|
+
`and properties.${propertyKey} = {propertyValue}`
|
|
210
|
+
].join(`
|
|
211
|
+
`),
|
|
212
|
+
values: { eventName, propertyValue: true }
|
|
213
|
+
});
|
|
214
|
+
return readSingleNumber(result);
|
|
215
|
+
}
|
|
216
|
+
async latestMetric(eventName, propertyKey) {
|
|
217
|
+
const result = await this.queryHogQL({
|
|
218
|
+
query: [
|
|
219
|
+
"select",
|
|
220
|
+
` properties.${propertyKey} as value`,
|
|
221
|
+
"from events",
|
|
222
|
+
"where event = {eventName}",
|
|
223
|
+
"order by timestamp desc",
|
|
224
|
+
"limit 1"
|
|
225
|
+
].join(`
|
|
226
|
+
`),
|
|
227
|
+
values: { eventName }
|
|
228
|
+
});
|
|
229
|
+
return readSingleNumber(result);
|
|
230
|
+
}
|
|
231
|
+
async queryHogQL(input) {
|
|
232
|
+
if (!this.reader.queryHogQL) {
|
|
233
|
+
throw new Error("Analytics reader does not support HogQL queries.");
|
|
234
|
+
}
|
|
235
|
+
return this.reader.queryHogQL(input);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function buildDateRange(days) {
|
|
239
|
+
const to = new Date;
|
|
240
|
+
const from = new Date(to.getTime() - days * 24 * 60 * 60 * 1000);
|
|
241
|
+
return { from, to };
|
|
242
|
+
}
|
|
243
|
+
function buildEventFilter(events, prefix) {
|
|
244
|
+
if (!events || events.length === 0)
|
|
245
|
+
return {};
|
|
246
|
+
if (events.length === 1) {
|
|
247
|
+
return {
|
|
248
|
+
clause: `event = {${prefix}0}`,
|
|
249
|
+
values: { [`${prefix}0`]: events[0] }
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const clauses = events.map((_event, index) => `event = {${prefix}${index}}`);
|
|
253
|
+
const values = {};
|
|
254
|
+
events.forEach((event, index) => {
|
|
255
|
+
values[`${prefix}${index}`] = event;
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
clause: `(${clauses.join(" or ")})`,
|
|
259
|
+
values
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function readSingleNumber(result) {
|
|
263
|
+
if (!Array.isArray(result.results) || result.results.length === 0) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const firstRow = result.results[0];
|
|
267
|
+
if (Array.isArray(firstRow) && firstRow.length > 0) {
|
|
268
|
+
const value = firstRow[0];
|
|
269
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
270
|
+
return value;
|
|
271
|
+
if (typeof value === "string" && value.trim()) {
|
|
272
|
+
const parsed = Number(value);
|
|
273
|
+
if (Number.isFinite(parsed))
|
|
274
|
+
return parsed;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
export {
|
|
280
|
+
trackLifecycleStageChange,
|
|
281
|
+
trackLifecycleAssessment,
|
|
282
|
+
metricsToSignals,
|
|
283
|
+
lifecycleEventNames,
|
|
284
|
+
createStageChangeEvent,
|
|
285
|
+
collectLifecycleMetrics,
|
|
286
|
+
PosthogLifecycleMetricSource
|
|
287
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/lifecycle/metric-collectors.ts
|
|
2
|
+
var collectLifecycleMetrics = async (source) => {
|
|
3
|
+
const [
|
|
4
|
+
activeUsers,
|
|
5
|
+
weeklyActiveUsers,
|
|
6
|
+
retentionRate,
|
|
7
|
+
monthlyRecurringRevenue,
|
|
8
|
+
customerCount,
|
|
9
|
+
teamSize,
|
|
10
|
+
burnMultiple
|
|
11
|
+
] = await Promise.all([
|
|
12
|
+
source.getActiveUsers(),
|
|
13
|
+
source.getWeeklyActiveUsers?.(),
|
|
14
|
+
source.getRetentionRate?.(),
|
|
15
|
+
source.getMonthlyRecurringRevenue?.(),
|
|
16
|
+
source.getCustomerCount?.(),
|
|
17
|
+
source.getTeamSize?.(),
|
|
18
|
+
source.getBurnMultiple?.()
|
|
19
|
+
]);
|
|
20
|
+
return {
|
|
21
|
+
activeUsers,
|
|
22
|
+
weeklyActiveUsers,
|
|
23
|
+
retentionRate,
|
|
24
|
+
monthlyRecurringRevenue,
|
|
25
|
+
customerCount,
|
|
26
|
+
teamSize,
|
|
27
|
+
burnMultiple
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
var metricsToSignals = (metrics, tenantId) => Object.entries(metrics).filter(([, value]) => value !== undefined && value !== null).map(([metricKey, value]) => ({
|
|
31
|
+
id: `lifecycle-metric:${metricKey}`,
|
|
32
|
+
kind: "metric",
|
|
33
|
+
source: "analytics",
|
|
34
|
+
name: metricKey,
|
|
35
|
+
value,
|
|
36
|
+
weight: 1,
|
|
37
|
+
confidence: 0.8,
|
|
38
|
+
details: tenantId ? { tenantId } : undefined,
|
|
39
|
+
capturedAt: new Date().toISOString()
|
|
40
|
+
}));
|
|
41
|
+
var lifecycleEventNames = {
|
|
42
|
+
assessmentRun: "lifecycle_assessment_run",
|
|
43
|
+
stageChanged: "lifecycle_stage_changed",
|
|
44
|
+
guidanceConsumed: "lifecycle_guidance_consumed"
|
|
45
|
+
};
|
|
46
|
+
var createStageChangeEvent = (payload) => ({
|
|
47
|
+
name: lifecycleEventNames.stageChanged,
|
|
48
|
+
userId: "system",
|
|
49
|
+
tenantId: payload.tenantId,
|
|
50
|
+
timestamp: new Date,
|
|
51
|
+
properties: { ...payload }
|
|
52
|
+
});
|
|
53
|
+
export {
|
|
54
|
+
metricsToSignals,
|
|
55
|
+
lifecycleEventNames,
|
|
56
|
+
createStageChangeEvent,
|
|
57
|
+
collectLifecycleMetrics
|
|
58
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/lifecycle/metric-collectors.ts
|
|
2
|
+
var collectLifecycleMetrics = async (source) => {
|
|
3
|
+
const [
|
|
4
|
+
activeUsers,
|
|
5
|
+
weeklyActiveUsers,
|
|
6
|
+
retentionRate,
|
|
7
|
+
monthlyRecurringRevenue,
|
|
8
|
+
customerCount,
|
|
9
|
+
teamSize,
|
|
10
|
+
burnMultiple
|
|
11
|
+
] = await Promise.all([
|
|
12
|
+
source.getActiveUsers(),
|
|
13
|
+
source.getWeeklyActiveUsers?.(),
|
|
14
|
+
source.getRetentionRate?.(),
|
|
15
|
+
source.getMonthlyRecurringRevenue?.(),
|
|
16
|
+
source.getCustomerCount?.(),
|
|
17
|
+
source.getTeamSize?.(),
|
|
18
|
+
source.getBurnMultiple?.()
|
|
19
|
+
]);
|
|
20
|
+
return {
|
|
21
|
+
activeUsers,
|
|
22
|
+
weeklyActiveUsers,
|
|
23
|
+
retentionRate,
|
|
24
|
+
monthlyRecurringRevenue,
|
|
25
|
+
customerCount,
|
|
26
|
+
teamSize,
|
|
27
|
+
burnMultiple
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
var metricsToSignals = (metrics, tenantId) => Object.entries(metrics).filter(([, value]) => value !== undefined && value !== null).map(([metricKey, value]) => ({
|
|
31
|
+
id: `lifecycle-metric:${metricKey}`,
|
|
32
|
+
kind: "metric",
|
|
33
|
+
source: "analytics",
|
|
34
|
+
name: metricKey,
|
|
35
|
+
value,
|
|
36
|
+
weight: 1,
|
|
37
|
+
confidence: 0.8,
|
|
38
|
+
details: tenantId ? { tenantId } : undefined,
|
|
39
|
+
capturedAt: new Date().toISOString()
|
|
40
|
+
}));
|
|
41
|
+
var lifecycleEventNames = {
|
|
42
|
+
assessmentRun: "lifecycle_assessment_run",
|
|
43
|
+
stageChanged: "lifecycle_stage_changed",
|
|
44
|
+
guidanceConsumed: "lifecycle_guidance_consumed"
|
|
45
|
+
};
|
|
46
|
+
var createStageChangeEvent = (payload) => ({
|
|
47
|
+
name: lifecycleEventNames.stageChanged,
|
|
48
|
+
userId: "system",
|
|
49
|
+
tenantId: payload.tenantId,
|
|
50
|
+
timestamp: new Date,
|
|
51
|
+
properties: { ...payload }
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// src/lifecycle/posthog-bridge.ts
|
|
55
|
+
var trackLifecycleAssessment = async (client, tenantId, assessment) => {
|
|
56
|
+
await client.capture({
|
|
57
|
+
distinctId: tenantId,
|
|
58
|
+
event: lifecycleEventNames.assessmentRun,
|
|
59
|
+
properties: {
|
|
60
|
+
stage: assessment.stage,
|
|
61
|
+
confidence: assessment.confidence,
|
|
62
|
+
axes: assessment.axes
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
var trackLifecycleStageChange = async (client, tenantId, previousStage, nextStage) => {
|
|
67
|
+
await client.capture({
|
|
68
|
+
distinctId: tenantId,
|
|
69
|
+
event: lifecycleEventNames.stageChanged,
|
|
70
|
+
properties: {
|
|
71
|
+
previousStage,
|
|
72
|
+
nextStage
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
export {
|
|
77
|
+
trackLifecycleStageChange,
|
|
78
|
+
trackLifecycleAssessment
|
|
79
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// src/lifecycle/posthog-metric-source.ts
|
|
2
|
+
class PosthogLifecycleMetricSource {
|
|
3
|
+
reader;
|
|
4
|
+
activityEvents;
|
|
5
|
+
retentionWindowDays;
|
|
6
|
+
revenueEvent;
|
|
7
|
+
revenueProperty;
|
|
8
|
+
customerEvent;
|
|
9
|
+
customerProperty;
|
|
10
|
+
teamSizeEvent;
|
|
11
|
+
teamSizeProperty;
|
|
12
|
+
burnMultipleEvent;
|
|
13
|
+
burnMultipleProperty;
|
|
14
|
+
constructor(reader, options = {}) {
|
|
15
|
+
this.reader = reader;
|
|
16
|
+
this.activityEvents = options.activityEvents;
|
|
17
|
+
this.retentionWindowDays = options.retentionWindowDays ?? 7;
|
|
18
|
+
this.revenueEvent = options.revenueEvent;
|
|
19
|
+
this.revenueProperty = options.revenueProperty ?? "amount";
|
|
20
|
+
this.customerEvent = options.customerEvent;
|
|
21
|
+
this.customerProperty = options.customerProperty ?? "is_customer";
|
|
22
|
+
this.teamSizeEvent = options.teamSizeEvent;
|
|
23
|
+
this.teamSizeProperty = options.teamSizeProperty ?? "team_size";
|
|
24
|
+
this.burnMultipleEvent = options.burnMultipleEvent;
|
|
25
|
+
this.burnMultipleProperty = options.burnMultipleProperty ?? "burn_multiple";
|
|
26
|
+
}
|
|
27
|
+
async getActiveUsers() {
|
|
28
|
+
return this.countDistinctUsers(1);
|
|
29
|
+
}
|
|
30
|
+
async getWeeklyActiveUsers() {
|
|
31
|
+
return this.countDistinctUsers(7);
|
|
32
|
+
}
|
|
33
|
+
async getRetentionRate() {
|
|
34
|
+
const windowDays = this.retentionWindowDays;
|
|
35
|
+
const now = new Date;
|
|
36
|
+
const prevEnd = new Date(now.getTime() - windowDays * 24 * 60 * 60 * 1000);
|
|
37
|
+
const prevStart = new Date(now.getTime() - windowDays * 2 * 24 * 60 * 60 * 1000);
|
|
38
|
+
const returningUsers = await this.countReturningUsers(prevStart, prevEnd, now);
|
|
39
|
+
const prevUsers = await this.countDistinctUsersBetween(prevStart, prevEnd);
|
|
40
|
+
if (prevUsers === undefined || prevUsers === 0)
|
|
41
|
+
return;
|
|
42
|
+
return returningUsers / prevUsers;
|
|
43
|
+
}
|
|
44
|
+
async getMonthlyRecurringRevenue() {
|
|
45
|
+
if (!this.revenueEvent || !this.revenueProperty)
|
|
46
|
+
return;
|
|
47
|
+
return this.sumMetric(this.revenueEvent, this.revenueProperty);
|
|
48
|
+
}
|
|
49
|
+
async getCustomerCount() {
|
|
50
|
+
if (!this.customerEvent || !this.customerProperty)
|
|
51
|
+
return;
|
|
52
|
+
return this.countDistinctUsersByProperty(this.customerEvent, this.customerProperty);
|
|
53
|
+
}
|
|
54
|
+
async getTeamSize() {
|
|
55
|
+
if (!this.teamSizeEvent || !this.teamSizeProperty)
|
|
56
|
+
return;
|
|
57
|
+
return this.latestMetric(this.teamSizeEvent, this.teamSizeProperty);
|
|
58
|
+
}
|
|
59
|
+
async getBurnMultiple() {
|
|
60
|
+
if (!this.burnMultipleEvent || !this.burnMultipleProperty)
|
|
61
|
+
return;
|
|
62
|
+
return this.latestMetric(this.burnMultipleEvent, this.burnMultipleProperty);
|
|
63
|
+
}
|
|
64
|
+
async countDistinctUsers(days) {
|
|
65
|
+
const range = buildDateRange(days);
|
|
66
|
+
return this.countDistinctUsersBetween(range.from, range.to);
|
|
67
|
+
}
|
|
68
|
+
async countDistinctUsersBetween(from, to) {
|
|
69
|
+
const eventFilter = buildEventFilter(this.activityEvents, "activityEvent");
|
|
70
|
+
const result = await this.queryHogQL({
|
|
71
|
+
query: [
|
|
72
|
+
"select",
|
|
73
|
+
" countDistinct(distinct_id) as total",
|
|
74
|
+
"from events",
|
|
75
|
+
`where timestamp >= {dateFrom} and timestamp < {dateTo}`,
|
|
76
|
+
eventFilter.clause ? `and ${eventFilter.clause}` : ""
|
|
77
|
+
].filter(Boolean).join(`
|
|
78
|
+
`),
|
|
79
|
+
values: {
|
|
80
|
+
dateFrom: from.toISOString(),
|
|
81
|
+
dateTo: to.toISOString(),
|
|
82
|
+
...eventFilter.values
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return readSingleNumber(result);
|
|
86
|
+
}
|
|
87
|
+
async countReturningUsers(previousStart, previousEnd, currentEnd) {
|
|
88
|
+
const eventFilter = buildEventFilter(this.activityEvents, "activityEvent");
|
|
89
|
+
const result = await this.queryHogQL({
|
|
90
|
+
query: [
|
|
91
|
+
"select",
|
|
92
|
+
" countDistinct(distinct_id) as total",
|
|
93
|
+
"from events",
|
|
94
|
+
`where timestamp >= {currentFrom} and timestamp < {currentTo}`,
|
|
95
|
+
eventFilter.clause ? `and ${eventFilter.clause}` : "",
|
|
96
|
+
"and distinct_id in (",
|
|
97
|
+
" select distinct_id from events",
|
|
98
|
+
" where timestamp >= {previousFrom} and timestamp < {previousTo}",
|
|
99
|
+
eventFilter.clause ? `and ${eventFilter.clause}` : "",
|
|
100
|
+
")"
|
|
101
|
+
].join(`
|
|
102
|
+
`),
|
|
103
|
+
values: {
|
|
104
|
+
currentFrom: previousEnd.toISOString(),
|
|
105
|
+
currentTo: currentEnd.toISOString(),
|
|
106
|
+
previousFrom: previousStart.toISOString(),
|
|
107
|
+
previousTo: previousEnd.toISOString(),
|
|
108
|
+
...eventFilter.values
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return readSingleNumber(result) ?? 0;
|
|
112
|
+
}
|
|
113
|
+
async sumMetric(eventName, propertyKey) {
|
|
114
|
+
const result = await this.queryHogQL({
|
|
115
|
+
query: [
|
|
116
|
+
"select",
|
|
117
|
+
` sum(properties.${propertyKey}) as total`,
|
|
118
|
+
"from events",
|
|
119
|
+
"where event = {eventName}"
|
|
120
|
+
].join(`
|
|
121
|
+
`),
|
|
122
|
+
values: { eventName }
|
|
123
|
+
});
|
|
124
|
+
return readSingleNumber(result);
|
|
125
|
+
}
|
|
126
|
+
async countDistinctUsersByProperty(eventName, propertyKey) {
|
|
127
|
+
const result = await this.queryHogQL({
|
|
128
|
+
query: [
|
|
129
|
+
"select",
|
|
130
|
+
" countDistinct(distinct_id) as total",
|
|
131
|
+
"from events",
|
|
132
|
+
"where event = {eventName}",
|
|
133
|
+
`and properties.${propertyKey} = {propertyValue}`
|
|
134
|
+
].join(`
|
|
135
|
+
`),
|
|
136
|
+
values: { eventName, propertyValue: true }
|
|
137
|
+
});
|
|
138
|
+
return readSingleNumber(result);
|
|
139
|
+
}
|
|
140
|
+
async latestMetric(eventName, propertyKey) {
|
|
141
|
+
const result = await this.queryHogQL({
|
|
142
|
+
query: [
|
|
143
|
+
"select",
|
|
144
|
+
` properties.${propertyKey} as value`,
|
|
145
|
+
"from events",
|
|
146
|
+
"where event = {eventName}",
|
|
147
|
+
"order by timestamp desc",
|
|
148
|
+
"limit 1"
|
|
149
|
+
].join(`
|
|
150
|
+
`),
|
|
151
|
+
values: { eventName }
|
|
152
|
+
});
|
|
153
|
+
return readSingleNumber(result);
|
|
154
|
+
}
|
|
155
|
+
async queryHogQL(input) {
|
|
156
|
+
if (!this.reader.queryHogQL) {
|
|
157
|
+
throw new Error("Analytics reader does not support HogQL queries.");
|
|
158
|
+
}
|
|
159
|
+
return this.reader.queryHogQL(input);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function buildDateRange(days) {
|
|
163
|
+
const to = new Date;
|
|
164
|
+
const from = new Date(to.getTime() - days * 24 * 60 * 60 * 1000);
|
|
165
|
+
return { from, to };
|
|
166
|
+
}
|
|
167
|
+
function buildEventFilter(events, prefix) {
|
|
168
|
+
if (!events || events.length === 0)
|
|
169
|
+
return {};
|
|
170
|
+
if (events.length === 1) {
|
|
171
|
+
return {
|
|
172
|
+
clause: `event = {${prefix}0}`,
|
|
173
|
+
values: { [`${prefix}0`]: events[0] }
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const clauses = events.map((_event, index) => `event = {${prefix}${index}}`);
|
|
177
|
+
const values = {};
|
|
178
|
+
events.forEach((event, index) => {
|
|
179
|
+
values[`${prefix}${index}`] = event;
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
clause: `(${clauses.join(" or ")})`,
|
|
183
|
+
values
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function readSingleNumber(result) {
|
|
187
|
+
if (!Array.isArray(result.results) || result.results.length === 0) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const firstRow = result.results[0];
|
|
191
|
+
if (Array.isArray(firstRow) && firstRow.length > 0) {
|
|
192
|
+
const value = firstRow[0];
|
|
193
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
194
|
+
return value;
|
|
195
|
+
if (typeof value === "string" && value.trim()) {
|
|
196
|
+
const parsed = Number(value);
|
|
197
|
+
if (Number.isFinite(parsed))
|
|
198
|
+
return parsed;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
export {
|
|
204
|
+
PosthogLifecycleMetricSource
|
|
205
|
+
};
|