@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
|
@@ -1,47 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
burnMultipleEvent?: string;
|
|
15
|
-
burnMultipleProperty?: string;
|
|
1
|
+
import type { AnalyticsReader } from '@contractspec/lib.contracts/integrations/providers/analytics';
|
|
2
|
+
import type { LifecycleMetricSource } from './metric-collectors';
|
|
3
|
+
export interface PosthogLifecycleMetricSourceOptions {
|
|
4
|
+
activityEvents?: string[];
|
|
5
|
+
retentionWindowDays?: number;
|
|
6
|
+
revenueEvent?: string;
|
|
7
|
+
revenueProperty?: string;
|
|
8
|
+
customerEvent?: string;
|
|
9
|
+
customerProperty?: string;
|
|
10
|
+
teamSizeEvent?: string;
|
|
11
|
+
teamSizeProperty?: string;
|
|
12
|
+
burnMultipleEvent?: string;
|
|
13
|
+
burnMultipleProperty?: string;
|
|
16
14
|
}
|
|
17
|
-
declare class PosthogLifecycleMetricSource implements LifecycleMetricSource {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
15
|
+
export declare class PosthogLifecycleMetricSource implements LifecycleMetricSource {
|
|
16
|
+
private readonly reader;
|
|
17
|
+
private readonly activityEvents?;
|
|
18
|
+
private readonly retentionWindowDays;
|
|
19
|
+
private readonly revenueEvent?;
|
|
20
|
+
private readonly revenueProperty?;
|
|
21
|
+
private readonly customerEvent?;
|
|
22
|
+
private readonly customerProperty?;
|
|
23
|
+
private readonly teamSizeEvent?;
|
|
24
|
+
private readonly teamSizeProperty?;
|
|
25
|
+
private readonly burnMultipleEvent?;
|
|
26
|
+
private readonly burnMultipleProperty?;
|
|
27
|
+
constructor(reader: AnalyticsReader, options?: PosthogLifecycleMetricSourceOptions);
|
|
28
|
+
getActiveUsers(): Promise<number | undefined>;
|
|
29
|
+
getWeeklyActiveUsers(): Promise<number | undefined>;
|
|
30
|
+
getRetentionRate(): Promise<number | undefined>;
|
|
31
|
+
getMonthlyRecurringRevenue(): Promise<number | undefined>;
|
|
32
|
+
getCustomerCount(): Promise<number | undefined>;
|
|
33
|
+
getTeamSize(): Promise<number | undefined>;
|
|
34
|
+
getBurnMultiple(): Promise<number | undefined>;
|
|
35
|
+
private countDistinctUsers;
|
|
36
|
+
private countDistinctUsersBetween;
|
|
37
|
+
private countReturningUsers;
|
|
38
|
+
private sumMetric;
|
|
39
|
+
private countDistinctUsersByProperty;
|
|
40
|
+
private latestMetric;
|
|
41
|
+
private queryHogQL;
|
|
44
42
|
}
|
|
45
|
-
//#endregion
|
|
46
|
-
export { PosthogLifecycleMetricSource, PosthogLifecycleMetricSourceOptions };
|
|
47
43
|
//# sourceMappingURL=posthog-metric-source.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"posthog-metric-source.d.ts","
|
|
1
|
+
{"version":3,"file":"posthog-metric-source.d.ts","sourceRoot":"","sources":["../../src/lifecycle/posthog-metric-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,eAAe,EAChB,MAAM,8DAA8D,CAAC;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjE,MAAM,WAAW,mCAAmC;IAClD,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,qBAAa,4BAA6B,YAAW,qBAAqB;IACxE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAW;IAC3C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAS;gBAG7C,MAAM,EAAE,eAAe,EACvB,OAAO,GAAE,mCAAwC;IAe7C,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAI7C,oBAAoB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAInD,gBAAgB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAkB/C,0BAA0B,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAKzD,gBAAgB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAQ/C,WAAW,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAK1C,eAAe,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;YAKtC,kBAAkB;YAKlB,yBAAyB;YAwBzB,mBAAmB;YA8BnB,SAAS;YAgBT,4BAA4B;YAiB5B,YAAY;YAkBZ,UAAU;CASzB"}
|
|
@@ -1,186 +1,206 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
1
|
+
// @bun
|
|
2
|
+
// src/lifecycle/posthog-metric-source.ts
|
|
3
|
+
class PosthogLifecycleMetricSource {
|
|
4
|
+
reader;
|
|
5
|
+
activityEvents;
|
|
6
|
+
retentionWindowDays;
|
|
7
|
+
revenueEvent;
|
|
8
|
+
revenueProperty;
|
|
9
|
+
customerEvent;
|
|
10
|
+
customerProperty;
|
|
11
|
+
teamSizeEvent;
|
|
12
|
+
teamSizeProperty;
|
|
13
|
+
burnMultipleEvent;
|
|
14
|
+
burnMultipleProperty;
|
|
15
|
+
constructor(reader, options = {}) {
|
|
16
|
+
this.reader = reader;
|
|
17
|
+
this.activityEvents = options.activityEvents;
|
|
18
|
+
this.retentionWindowDays = options.retentionWindowDays ?? 7;
|
|
19
|
+
this.revenueEvent = options.revenueEvent;
|
|
20
|
+
this.revenueProperty = options.revenueProperty ?? "amount";
|
|
21
|
+
this.customerEvent = options.customerEvent;
|
|
22
|
+
this.customerProperty = options.customerProperty ?? "is_customer";
|
|
23
|
+
this.teamSizeEvent = options.teamSizeEvent;
|
|
24
|
+
this.teamSizeProperty = options.teamSizeProperty ?? "team_size";
|
|
25
|
+
this.burnMultipleEvent = options.burnMultipleEvent;
|
|
26
|
+
this.burnMultipleProperty = options.burnMultipleProperty ?? "burn_multiple";
|
|
27
|
+
}
|
|
28
|
+
async getActiveUsers() {
|
|
29
|
+
return this.countDistinctUsers(1);
|
|
30
|
+
}
|
|
31
|
+
async getWeeklyActiveUsers() {
|
|
32
|
+
return this.countDistinctUsers(7);
|
|
33
|
+
}
|
|
34
|
+
async getRetentionRate() {
|
|
35
|
+
const windowDays = this.retentionWindowDays;
|
|
36
|
+
const now = new Date;
|
|
37
|
+
const prevEnd = new Date(now.getTime() - windowDays * 24 * 60 * 60 * 1000);
|
|
38
|
+
const prevStart = new Date(now.getTime() - windowDays * 2 * 24 * 60 * 60 * 1000);
|
|
39
|
+
const returningUsers = await this.countReturningUsers(prevStart, prevEnd, now);
|
|
40
|
+
const prevUsers = await this.countDistinctUsersBetween(prevStart, prevEnd);
|
|
41
|
+
if (prevUsers === undefined || prevUsers === 0)
|
|
42
|
+
return;
|
|
43
|
+
return returningUsers / prevUsers;
|
|
44
|
+
}
|
|
45
|
+
async getMonthlyRecurringRevenue() {
|
|
46
|
+
if (!this.revenueEvent || !this.revenueProperty)
|
|
47
|
+
return;
|
|
48
|
+
return this.sumMetric(this.revenueEvent, this.revenueProperty);
|
|
49
|
+
}
|
|
50
|
+
async getCustomerCount() {
|
|
51
|
+
if (!this.customerEvent || !this.customerProperty)
|
|
52
|
+
return;
|
|
53
|
+
return this.countDistinctUsersByProperty(this.customerEvent, this.customerProperty);
|
|
54
|
+
}
|
|
55
|
+
async getTeamSize() {
|
|
56
|
+
if (!this.teamSizeEvent || !this.teamSizeProperty)
|
|
57
|
+
return;
|
|
58
|
+
return this.latestMetric(this.teamSizeEvent, this.teamSizeProperty);
|
|
59
|
+
}
|
|
60
|
+
async getBurnMultiple() {
|
|
61
|
+
if (!this.burnMultipleEvent || !this.burnMultipleProperty)
|
|
62
|
+
return;
|
|
63
|
+
return this.latestMetric(this.burnMultipleEvent, this.burnMultipleProperty);
|
|
64
|
+
}
|
|
65
|
+
async countDistinctUsers(days) {
|
|
66
|
+
const range = buildDateRange(days);
|
|
67
|
+
return this.countDistinctUsersBetween(range.from, range.to);
|
|
68
|
+
}
|
|
69
|
+
async countDistinctUsersBetween(from, to) {
|
|
70
|
+
const eventFilter = buildEventFilter(this.activityEvents, "activityEvent");
|
|
71
|
+
const result = await this.queryHogQL({
|
|
72
|
+
query: [
|
|
73
|
+
"select",
|
|
74
|
+
" countDistinct(distinct_id) as total",
|
|
75
|
+
"from events",
|
|
76
|
+
`where timestamp >= {dateFrom} and timestamp < {dateTo}`,
|
|
77
|
+
eventFilter.clause ? `and ${eventFilter.clause}` : ""
|
|
78
|
+
].filter(Boolean).join(`
|
|
79
|
+
`),
|
|
80
|
+
values: {
|
|
81
|
+
dateFrom: from.toISOString(),
|
|
82
|
+
dateTo: to.toISOString(),
|
|
83
|
+
...eventFilter.values
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return readSingleNumber(result);
|
|
87
|
+
}
|
|
88
|
+
async countReturningUsers(previousStart, previousEnd, currentEnd) {
|
|
89
|
+
const eventFilter = buildEventFilter(this.activityEvents, "activityEvent");
|
|
90
|
+
const result = await this.queryHogQL({
|
|
91
|
+
query: [
|
|
92
|
+
"select",
|
|
93
|
+
" countDistinct(distinct_id) as total",
|
|
94
|
+
"from events",
|
|
95
|
+
`where timestamp >= {currentFrom} and timestamp < {currentTo}`,
|
|
96
|
+
eventFilter.clause ? `and ${eventFilter.clause}` : "",
|
|
97
|
+
"and distinct_id in (",
|
|
98
|
+
" select distinct_id from events",
|
|
99
|
+
" where timestamp >= {previousFrom} and timestamp < {previousTo}",
|
|
100
|
+
eventFilter.clause ? `and ${eventFilter.clause}` : "",
|
|
101
|
+
")"
|
|
102
|
+
].join(`
|
|
103
|
+
`),
|
|
104
|
+
values: {
|
|
105
|
+
currentFrom: previousEnd.toISOString(),
|
|
106
|
+
currentTo: currentEnd.toISOString(),
|
|
107
|
+
previousFrom: previousStart.toISOString(),
|
|
108
|
+
previousTo: previousEnd.toISOString(),
|
|
109
|
+
...eventFilter.values
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return readSingleNumber(result) ?? 0;
|
|
113
|
+
}
|
|
114
|
+
async sumMetric(eventName, propertyKey) {
|
|
115
|
+
const result = await this.queryHogQL({
|
|
116
|
+
query: [
|
|
117
|
+
"select",
|
|
118
|
+
` sum(properties.${propertyKey}) as total`,
|
|
119
|
+
"from events",
|
|
120
|
+
"where event = {eventName}"
|
|
121
|
+
].join(`
|
|
122
|
+
`),
|
|
123
|
+
values: { eventName }
|
|
124
|
+
});
|
|
125
|
+
return readSingleNumber(result);
|
|
126
|
+
}
|
|
127
|
+
async countDistinctUsersByProperty(eventName, propertyKey) {
|
|
128
|
+
const result = await this.queryHogQL({
|
|
129
|
+
query: [
|
|
130
|
+
"select",
|
|
131
|
+
" countDistinct(distinct_id) as total",
|
|
132
|
+
"from events",
|
|
133
|
+
"where event = {eventName}",
|
|
134
|
+
`and properties.${propertyKey} = {propertyValue}`
|
|
135
|
+
].join(`
|
|
136
|
+
`),
|
|
137
|
+
values: { eventName, propertyValue: true }
|
|
138
|
+
});
|
|
139
|
+
return readSingleNumber(result);
|
|
140
|
+
}
|
|
141
|
+
async latestMetric(eventName, propertyKey) {
|
|
142
|
+
const result = await this.queryHogQL({
|
|
143
|
+
query: [
|
|
144
|
+
"select",
|
|
145
|
+
` properties.${propertyKey} as value`,
|
|
146
|
+
"from events",
|
|
147
|
+
"where event = {eventName}",
|
|
148
|
+
"order by timestamp desc",
|
|
149
|
+
"limit 1"
|
|
150
|
+
].join(`
|
|
151
|
+
`),
|
|
152
|
+
values: { eventName }
|
|
153
|
+
});
|
|
154
|
+
return readSingleNumber(result);
|
|
155
|
+
}
|
|
156
|
+
async queryHogQL(input) {
|
|
157
|
+
if (!this.reader.queryHogQL) {
|
|
158
|
+
throw new Error("Analytics reader does not support HogQL queries.");
|
|
159
|
+
}
|
|
160
|
+
return this.reader.queryHogQL(input);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
148
163
|
function buildDateRange(days) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
to
|
|
153
|
-
};
|
|
164
|
+
const to = new Date;
|
|
165
|
+
const from = new Date(to.getTime() - days * 24 * 60 * 60 * 1000);
|
|
166
|
+
return { from, to };
|
|
154
167
|
}
|
|
155
168
|
function buildEventFilter(events, prefix) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
if (!events || events.length === 0)
|
|
170
|
+
return {};
|
|
171
|
+
if (events.length === 1) {
|
|
172
|
+
return {
|
|
173
|
+
clause: `event = {${prefix}0}`,
|
|
174
|
+
values: { [`${prefix}0`]: events[0] }
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const clauses = events.map((_event, index) => `event = {${prefix}${index}}`);
|
|
178
|
+
const values = {};
|
|
179
|
+
events.forEach((event, index) => {
|
|
180
|
+
values[`${prefix}${index}`] = event;
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
clause: `(${clauses.join(" or ")})`,
|
|
184
|
+
values
|
|
185
|
+
};
|
|
170
186
|
}
|
|
171
187
|
function readSingleNumber(result) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
188
|
+
if (!Array.isArray(result.results) || result.results.length === 0) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const firstRow = result.results[0];
|
|
192
|
+
if (Array.isArray(firstRow) && firstRow.length > 0) {
|
|
193
|
+
const value = firstRow[0];
|
|
194
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
195
|
+
return value;
|
|
196
|
+
if (typeof value === "string" && value.trim()) {
|
|
197
|
+
const parsed = Number(value);
|
|
198
|
+
if (Number.isFinite(parsed))
|
|
199
|
+
return parsed;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
182
203
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
//# sourceMappingURL=posthog-metric-source.js.map
|
|
204
|
+
export {
|
|
205
|
+
PosthogLifecycleMetricSource
|
|
206
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// src/churn/predictor.ts
|
|
2
|
+
import dayjs from "dayjs";
|
|
3
|
+
|
|
4
|
+
class ChurnPredictor {
|
|
5
|
+
recencyWeight;
|
|
6
|
+
frequencyWeight;
|
|
7
|
+
errorWeight;
|
|
8
|
+
decayDays;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.recencyWeight = options?.recencyWeight ?? 0.5;
|
|
11
|
+
this.frequencyWeight = options?.frequencyWeight ?? 0.3;
|
|
12
|
+
this.errorWeight = options?.errorWeight ?? 0.2;
|
|
13
|
+
this.decayDays = options?.decayDays ?? 14;
|
|
14
|
+
}
|
|
15
|
+
score(events) {
|
|
16
|
+
const grouped = groupBy(events, (event) => event.userId);
|
|
17
|
+
const signals = [];
|
|
18
|
+
for (const [userId, userEvents] of grouped.entries()) {
|
|
19
|
+
const score = this.computeScore(userEvents);
|
|
20
|
+
signals.push({
|
|
21
|
+
userId,
|
|
22
|
+
score,
|
|
23
|
+
bucket: score >= 0.7 ? "high" : score >= 0.4 ? "medium" : "low",
|
|
24
|
+
drivers: this.drivers(userEvents)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return signals.sort((a, b) => b.score - a.score);
|
|
28
|
+
}
|
|
29
|
+
computeScore(events) {
|
|
30
|
+
if (!events.length)
|
|
31
|
+
return 0;
|
|
32
|
+
const sorted = events.sort((a, b) => dateMs(a) - dateMs(b));
|
|
33
|
+
const lastEvent = sorted[sorted.length - 1];
|
|
34
|
+
if (!lastEvent)
|
|
35
|
+
return 0;
|
|
36
|
+
const daysSinceLast = dayjs().diff(dayjs(lastEvent.timestamp), "day");
|
|
37
|
+
const recencyScore = Math.max(0, 1 - daysSinceLast / this.decayDays);
|
|
38
|
+
const windowStart = dayjs().subtract(this.decayDays, "day");
|
|
39
|
+
const recentEvents = sorted.filter((event) => dayjs(event.timestamp).isAfter(windowStart));
|
|
40
|
+
const averagePerDay = recentEvents.length / Math.max(this.decayDays, 1);
|
|
41
|
+
const frequencyScore = Math.min(1, averagePerDay * 5);
|
|
42
|
+
const errorEvents = recentEvents.filter((event) => typeof event.properties?.error !== "undefined" || /error|failed/i.test(event.name)).length;
|
|
43
|
+
const errorScore = Math.min(1, errorEvents / 3);
|
|
44
|
+
const score = recencyScore * this.recencyWeight + frequencyScore * this.frequencyWeight + (1 - errorScore) * this.errorWeight;
|
|
45
|
+
return Number(score.toFixed(3));
|
|
46
|
+
}
|
|
47
|
+
drivers(events) {
|
|
48
|
+
const drivers = [];
|
|
49
|
+
const sorted = events.sort((a, b) => dateMs(a) - dateMs(b));
|
|
50
|
+
const lastEvent = sorted[sorted.length - 1];
|
|
51
|
+
if (lastEvent) {
|
|
52
|
+
const days = dayjs().diff(dayjs(lastEvent.timestamp), "day");
|
|
53
|
+
if (days > this.decayDays)
|
|
54
|
+
drivers.push(`Inactive for ${days} days`);
|
|
55
|
+
}
|
|
56
|
+
const errorEvents = events.filter((event) => typeof event.properties?.error !== "undefined" || /error|failed/i.test(event.name));
|
|
57
|
+
if (errorEvents.length)
|
|
58
|
+
drivers.push(`${errorEvents.length} errors logged`);
|
|
59
|
+
return drivers;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function groupBy(items, selector) {
|
|
63
|
+
const map = new Map;
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
const key = selector(item);
|
|
66
|
+
const list = map.get(key) ?? [];
|
|
67
|
+
list.push(item);
|
|
68
|
+
map.set(key, list);
|
|
69
|
+
}
|
|
70
|
+
return map;
|
|
71
|
+
}
|
|
72
|
+
function dateMs(event) {
|
|
73
|
+
return new Date(event.timestamp).getTime();
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
ChurnPredictor
|
|
77
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// src/churn/predictor.ts
|
|
2
|
+
import dayjs from "dayjs";
|
|
3
|
+
|
|
4
|
+
class ChurnPredictor {
|
|
5
|
+
recencyWeight;
|
|
6
|
+
frequencyWeight;
|
|
7
|
+
errorWeight;
|
|
8
|
+
decayDays;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.recencyWeight = options?.recencyWeight ?? 0.5;
|
|
11
|
+
this.frequencyWeight = options?.frequencyWeight ?? 0.3;
|
|
12
|
+
this.errorWeight = options?.errorWeight ?? 0.2;
|
|
13
|
+
this.decayDays = options?.decayDays ?? 14;
|
|
14
|
+
}
|
|
15
|
+
score(events) {
|
|
16
|
+
const grouped = groupBy(events, (event) => event.userId);
|
|
17
|
+
const signals = [];
|
|
18
|
+
for (const [userId, userEvents] of grouped.entries()) {
|
|
19
|
+
const score = this.computeScore(userEvents);
|
|
20
|
+
signals.push({
|
|
21
|
+
userId,
|
|
22
|
+
score,
|
|
23
|
+
bucket: score >= 0.7 ? "high" : score >= 0.4 ? "medium" : "low",
|
|
24
|
+
drivers: this.drivers(userEvents)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return signals.sort((a, b) => b.score - a.score);
|
|
28
|
+
}
|
|
29
|
+
computeScore(events) {
|
|
30
|
+
if (!events.length)
|
|
31
|
+
return 0;
|
|
32
|
+
const sorted = events.sort((a, b) => dateMs(a) - dateMs(b));
|
|
33
|
+
const lastEvent = sorted[sorted.length - 1];
|
|
34
|
+
if (!lastEvent)
|
|
35
|
+
return 0;
|
|
36
|
+
const daysSinceLast = dayjs().diff(dayjs(lastEvent.timestamp), "day");
|
|
37
|
+
const recencyScore = Math.max(0, 1 - daysSinceLast / this.decayDays);
|
|
38
|
+
const windowStart = dayjs().subtract(this.decayDays, "day");
|
|
39
|
+
const recentEvents = sorted.filter((event) => dayjs(event.timestamp).isAfter(windowStart));
|
|
40
|
+
const averagePerDay = recentEvents.length / Math.max(this.decayDays, 1);
|
|
41
|
+
const frequencyScore = Math.min(1, averagePerDay * 5);
|
|
42
|
+
const errorEvents = recentEvents.filter((event) => typeof event.properties?.error !== "undefined" || /error|failed/i.test(event.name)).length;
|
|
43
|
+
const errorScore = Math.min(1, errorEvents / 3);
|
|
44
|
+
const score = recencyScore * this.recencyWeight + frequencyScore * this.frequencyWeight + (1 - errorScore) * this.errorWeight;
|
|
45
|
+
return Number(score.toFixed(3));
|
|
46
|
+
}
|
|
47
|
+
drivers(events) {
|
|
48
|
+
const drivers = [];
|
|
49
|
+
const sorted = events.sort((a, b) => dateMs(a) - dateMs(b));
|
|
50
|
+
const lastEvent = sorted[sorted.length - 1];
|
|
51
|
+
if (lastEvent) {
|
|
52
|
+
const days = dayjs().diff(dayjs(lastEvent.timestamp), "day");
|
|
53
|
+
if (days > this.decayDays)
|
|
54
|
+
drivers.push(`Inactive for ${days} days`);
|
|
55
|
+
}
|
|
56
|
+
const errorEvents = events.filter((event) => typeof event.properties?.error !== "undefined" || /error|failed/i.test(event.name));
|
|
57
|
+
if (errorEvents.length)
|
|
58
|
+
drivers.push(`${errorEvents.length} errors logged`);
|
|
59
|
+
return drivers;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function groupBy(items, selector) {
|
|
63
|
+
const map = new Map;
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
const key = selector(item);
|
|
66
|
+
const list = map.get(key) ?? [];
|
|
67
|
+
list.push(item);
|
|
68
|
+
map.set(key, list);
|
|
69
|
+
}
|
|
70
|
+
return map;
|
|
71
|
+
}
|
|
72
|
+
function dateMs(event) {
|
|
73
|
+
return new Date(event.timestamp).getTime();
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
ChurnPredictor
|
|
77
|
+
};
|