@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.
Files changed (89) hide show
  1. package/dist/browser/churn/index.js +77 -0
  2. package/dist/browser/churn/predictor.js +77 -0
  3. package/dist/browser/cohort/index.js +117 -0
  4. package/dist/browser/cohort/tracker.js +117 -0
  5. package/dist/browser/funnel/analyzer.js +68 -0
  6. package/dist/browser/funnel/index.js +68 -0
  7. package/dist/browser/growth/hypothesis-generator.js +46 -0
  8. package/dist/browser/growth/index.js +46 -0
  9. package/dist/browser/index.js +723 -0
  10. package/dist/browser/lifecycle/index.js +287 -0
  11. package/dist/browser/lifecycle/metric-collectors.js +58 -0
  12. package/dist/browser/lifecycle/posthog-bridge.js +79 -0
  13. package/dist/browser/lifecycle/posthog-metric-source.js +205 -0
  14. package/dist/browser/posthog/event-source.js +138 -0
  15. package/dist/browser/posthog/index.js +138 -0
  16. package/dist/browser/types.js +0 -0
  17. package/dist/churn/index.d.ts +2 -2
  18. package/dist/churn/index.d.ts.map +1 -0
  19. package/dist/churn/index.js +77 -2
  20. package/dist/churn/predictor.d.ts +15 -19
  21. package/dist/churn/predictor.d.ts.map +1 -1
  22. package/dist/churn/predictor.js +72 -68
  23. package/dist/cohort/index.d.ts +2 -2
  24. package/dist/cohort/index.d.ts.map +1 -0
  25. package/dist/cohort/index.js +117 -2
  26. package/dist/cohort/tracker.d.ts +3 -7
  27. package/dist/cohort/tracker.d.ts.map +1 -1
  28. package/dist/cohort/tracker.js +106 -87
  29. package/dist/funnel/analyzer.d.ts +4 -8
  30. package/dist/funnel/analyzer.d.ts.map +1 -1
  31. package/dist/funnel/analyzer.js +67 -62
  32. package/dist/funnel/index.d.ts +2 -2
  33. package/dist/funnel/index.d.ts.map +1 -0
  34. package/dist/funnel/index.js +69 -3
  35. package/dist/growth/hypothesis-generator.d.ts +11 -15
  36. package/dist/growth/hypothesis-generator.d.ts.map +1 -1
  37. package/dist/growth/hypothesis-generator.js +46 -39
  38. package/dist/growth/index.d.ts +2 -2
  39. package/dist/growth/index.d.ts.map +1 -0
  40. package/dist/growth/index.js +47 -3
  41. package/dist/index.d.ts +8 -10
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +724 -12
  44. package/dist/lifecycle/index.d.ts +4 -4
  45. package/dist/lifecycle/index.d.ts.map +1 -0
  46. package/dist/lifecycle/index.js +287 -4
  47. package/dist/lifecycle/metric-collectors.d.ts +22 -26
  48. package/dist/lifecycle/metric-collectors.d.ts.map +1 -1
  49. package/dist/lifecycle/metric-collectors.js +55 -44
  50. package/dist/lifecycle/posthog-bridge.d.ts +9 -13
  51. package/dist/lifecycle/posthog-bridge.d.ts.map +1 -1
  52. package/dist/lifecycle/posthog-bridge.js +77 -25
  53. package/dist/lifecycle/posthog-metric-source.d.ts +40 -44
  54. package/dist/lifecycle/posthog-metric-source.d.ts.map +1 -1
  55. package/dist/lifecycle/posthog-metric-source.js +200 -180
  56. package/dist/node/churn/index.js +77 -0
  57. package/dist/node/churn/predictor.js +77 -0
  58. package/dist/node/cohort/index.js +117 -0
  59. package/dist/node/cohort/tracker.js +117 -0
  60. package/dist/node/funnel/analyzer.js +68 -0
  61. package/dist/node/funnel/index.js +68 -0
  62. package/dist/node/growth/hypothesis-generator.js +46 -0
  63. package/dist/node/growth/index.js +46 -0
  64. package/dist/node/index.js +723 -0
  65. package/dist/node/lifecycle/index.js +287 -0
  66. package/dist/node/lifecycle/metric-collectors.js +58 -0
  67. package/dist/node/lifecycle/posthog-bridge.js +79 -0
  68. package/dist/node/lifecycle/posthog-metric-source.js +205 -0
  69. package/dist/node/posthog/event-source.js +138 -0
  70. package/dist/node/posthog/index.js +138 -0
  71. package/dist/node/types.js +0 -0
  72. package/dist/posthog/event-source.d.ts +18 -22
  73. package/dist/posthog/event-source.d.ts.map +1 -1
  74. package/dist/posthog/event-source.js +131 -111
  75. package/dist/posthog/index.d.ts +2 -2
  76. package/dist/posthog/index.d.ts.map +1 -0
  77. package/dist/posthog/index.js +139 -3
  78. package/dist/types.d.ts +52 -55
  79. package/dist/types.d.ts.map +1 -1
  80. package/dist/types.js +1 -0
  81. package/package.json +189 -46
  82. package/dist/churn/predictor.js.map +0 -1
  83. package/dist/cohort/tracker.js.map +0 -1
  84. package/dist/funnel/analyzer.js.map +0 -1
  85. package/dist/growth/hypothesis-generator.js.map +0 -1
  86. package/dist/lifecycle/metric-collectors.js.map +0 -1
  87. package/dist/lifecycle/posthog-bridge.js.map +0 -1
  88. package/dist/lifecycle/posthog-metric-source.js.map +0 -1
  89. 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
+ };