@contractspec/lib.analytics 1.57.0 → 1.58.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
@@ -1,47 +1,43 @@
1
- import { LifecycleMetricSource } from "./metric-collectors.js";
2
- import { AnalyticsReader } from "@contractspec/lib.contracts/integrations/providers/analytics";
3
-
4
- //#region src/lifecycle/posthog-metric-source.d.ts
5
- interface PosthogLifecycleMetricSourceOptions {
6
- activityEvents?: string[];
7
- retentionWindowDays?: number;
8
- revenueEvent?: string;
9
- revenueProperty?: string;
10
- customerEvent?: string;
11
- customerProperty?: string;
12
- teamSizeEvent?: string;
13
- teamSizeProperty?: string;
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
- private readonly reader;
19
- private readonly activityEvents?;
20
- private readonly retentionWindowDays;
21
- private readonly revenueEvent?;
22
- private readonly revenueProperty?;
23
- private readonly customerEvent?;
24
- private readonly customerProperty?;
25
- private readonly teamSizeEvent?;
26
- private readonly teamSizeProperty?;
27
- private readonly burnMultipleEvent?;
28
- private readonly burnMultipleProperty?;
29
- constructor(reader: AnalyticsReader, options?: PosthogLifecycleMetricSourceOptions);
30
- getActiveUsers(): Promise<number | undefined>;
31
- getWeeklyActiveUsers(): Promise<number | undefined>;
32
- getRetentionRate(): Promise<number | undefined>;
33
- getMonthlyRecurringRevenue(): Promise<number | undefined>;
34
- getCustomerCount(): Promise<number | undefined>;
35
- getTeamSize(): Promise<number | undefined>;
36
- getBurnMultiple(): Promise<number | undefined>;
37
- private countDistinctUsers;
38
- private countDistinctUsersBetween;
39
- private countReturningUsers;
40
- private sumMetric;
41
- private countDistinctUsersByProperty;
42
- private latestMetric;
43
- private queryHogQL;
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","names":[],"sources":["../../src/lifecycle/posthog-metric-source.ts"],"mappings":";;;;UAMiB,mCAAA;EACf,cAAA;EACA,mBAAA;EACA,YAAA;EACA,eAAA;EACA,aAAA;EACA,gBAAA;EACA,aAAA;EACA,gBAAA;EACA,iBAAA;EACA,oBAAA;AAAA;AAAA,cAGW,4BAAA,YAAwC,qBAAA;EAAA,iBAClC,MAAA;EAAA,iBACA,cAAA;EAAA,iBACA,mBAAA;EAAA,iBACA,YAAA;EAAA,iBACA,eAAA;EAAA,iBACA,aAAA;EAAA,iBACA,gBAAA;EAAA,iBACA,aAAA;EAAA,iBACA,gBAAA;EAAA,iBACA,iBAAA;EAAA,iBACA,oBAAA;cAGf,MAAA,EAAQ,eAAA,EACR,OAAA,GAAS,mCAAA;EAeL,cAAA,CAAA,GAAkB,OAAA;EAIlB,oBAAA,CAAA,GAAwB,OAAA;EAIxB,gBAAA,CAAA,GAAoB,OAAA;EAkBpB,0BAAA,CAAA,GAA8B,OAAA;EAK9B,gBAAA,CAAA,GAAoB,OAAA;EAQpB,WAAA,CAAA,GAAe,OAAA;EAKf,eAAA,CAAA,GAAmB,OAAA;EAAA,QAKX,kBAAA;EAAA,QAKA,yBAAA;EAAA,QAwBA,mBAAA;EAAA,QA8BA,SAAA;EAAA,QAgBA,4BAAA;EAAA,QAiBA,YAAA;EAAA,QAkBA,UAAA;AAAA"}
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
- //#region src/lifecycle/posthog-metric-source.ts
2
- var PosthogLifecycleMetricSource = class {
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 = /* @__PURE__ */ new Date();
36
- const prevEnd = /* @__PURE__ */ new Date(now.getTime() - windowDays * 24 * 60 * 60 * 1e3);
37
- const prevStart = /* @__PURE__ */ new Date(now.getTime() - windowDays * 2 * 24 * 60 * 60 * 1e3);
38
- const returningUsers = await this.countReturningUsers(prevStart, prevEnd, now);
39
- const prevUsers = await this.countDistinctUsersBetween(prevStart, prevEnd);
40
- if (prevUsers === void 0 || prevUsers === 0) return void 0;
41
- return returningUsers / prevUsers;
42
- }
43
- async getMonthlyRecurringRevenue() {
44
- if (!this.revenueEvent || !this.revenueProperty) return void 0;
45
- return this.sumMetric(this.revenueEvent, this.revenueProperty);
46
- }
47
- async getCustomerCount() {
48
- if (!this.customerEvent || !this.customerProperty) return void 0;
49
- return this.countDistinctUsersByProperty(this.customerEvent, this.customerProperty);
50
- }
51
- async getTeamSize() {
52
- if (!this.teamSizeEvent || !this.teamSizeProperty) return void 0;
53
- return this.latestMetric(this.teamSizeEvent, this.teamSizeProperty);
54
- }
55
- async getBurnMultiple() {
56
- if (!this.burnMultipleEvent || !this.burnMultipleProperty) return void 0;
57
- return this.latestMetric(this.burnMultipleEvent, this.burnMultipleProperty);
58
- }
59
- async countDistinctUsers(days) {
60
- const range = buildDateRange(days);
61
- return this.countDistinctUsersBetween(range.from, range.to);
62
- }
63
- async countDistinctUsersBetween(from, to) {
64
- const eventFilter = buildEventFilter(this.activityEvents, "activityEvent");
65
- return readSingleNumber(await this.queryHogQL({
66
- query: [
67
- "select",
68
- " countDistinct(distinct_id) as total",
69
- "from events",
70
- `where timestamp >= {dateFrom} and timestamp < {dateTo}`,
71
- eventFilter.clause ? `and ${eventFilter.clause}` : ""
72
- ].filter(Boolean).join("\n"),
73
- values: {
74
- dateFrom: from.toISOString(),
75
- dateTo: to.toISOString(),
76
- ...eventFilter.values
77
- }
78
- }));
79
- }
80
- async countReturningUsers(previousStart, previousEnd, currentEnd) {
81
- const eventFilter = buildEventFilter(this.activityEvents, "activityEvent");
82
- return readSingleNumber(await this.queryHogQL({
83
- query: [
84
- "select",
85
- " countDistinct(distinct_id) as total",
86
- "from events",
87
- `where timestamp >= {currentFrom} and timestamp < {currentTo}`,
88
- eventFilter.clause ? `and ${eventFilter.clause}` : "",
89
- "and distinct_id in (",
90
- " select distinct_id from events",
91
- " where timestamp >= {previousFrom} and timestamp < {previousTo}",
92
- eventFilter.clause ? `and ${eventFilter.clause}` : "",
93
- ")"
94
- ].join("\n"),
95
- values: {
96
- currentFrom: previousEnd.toISOString(),
97
- currentTo: currentEnd.toISOString(),
98
- previousFrom: previousStart.toISOString(),
99
- previousTo: previousEnd.toISOString(),
100
- ...eventFilter.values
101
- }
102
- })) ?? 0;
103
- }
104
- async sumMetric(eventName, propertyKey) {
105
- return readSingleNumber(await this.queryHogQL({
106
- query: [
107
- "select",
108
- ` sum(properties.${propertyKey}) as total`,
109
- "from events",
110
- "where event = {eventName}"
111
- ].join("\n"),
112
- values: { eventName }
113
- }));
114
- }
115
- async countDistinctUsersByProperty(eventName, propertyKey) {
116
- return readSingleNumber(await this.queryHogQL({
117
- query: [
118
- "select",
119
- " countDistinct(distinct_id) as total",
120
- "from events",
121
- "where event = {eventName}",
122
- `and properties.${propertyKey} = {propertyValue}`
123
- ].join("\n"),
124
- values: {
125
- eventName,
126
- propertyValue: true
127
- }
128
- }));
129
- }
130
- async latestMetric(eventName, propertyKey) {
131
- return readSingleNumber(await this.queryHogQL({
132
- query: [
133
- "select",
134
- ` properties.${propertyKey} as value`,
135
- "from events",
136
- "where event = {eventName}",
137
- "order by timestamp desc",
138
- "limit 1"
139
- ].join("\n"),
140
- values: { eventName }
141
- }));
142
- }
143
- async queryHogQL(input) {
144
- if (!this.reader.queryHogQL) throw new Error("Analytics reader does not support HogQL queries.");
145
- return this.reader.queryHogQL(input);
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
- const to = /* @__PURE__ */ new Date();
150
- return {
151
- from: /* @__PURE__ */ new Date(to.getTime() - days * 24 * 60 * 60 * 1e3),
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
- if (!events || events.length === 0) return {};
157
- if (events.length === 1) return {
158
- clause: `event = {${prefix}0}`,
159
- values: { [`${prefix}0`]: events[0] }
160
- };
161
- const clauses = events.map((_event, index) => `event = {${prefix}${index}}`);
162
- const values = {};
163
- events.forEach((event, index) => {
164
- values[`${prefix}${index}`] = event;
165
- });
166
- return {
167
- clause: `(${clauses.join(" or ")})`,
168
- values
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
- if (!Array.isArray(result.results) || result.results.length === 0) return;
173
- const firstRow = result.results[0];
174
- if (Array.isArray(firstRow) && firstRow.length > 0) {
175
- const value = firstRow[0];
176
- if (typeof value === "number" && Number.isFinite(value)) return value;
177
- if (typeof value === "string" && value.trim()) {
178
- const parsed = Number(value);
179
- if (Number.isFinite(parsed)) return parsed;
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
- //#endregion
185
- export { PosthogLifecycleMetricSource };
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
+ };