@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,138 @@
1
+ // src/posthog/event-source.ts
2
+ class PosthogAnalyticsEventSource {
3
+ reader;
4
+ limitPerEvent;
5
+ tenantPropertyKey;
6
+ defaultRangeDays;
7
+ constructor(reader, options = {}) {
8
+ this.reader = reader;
9
+ this.limitPerEvent = options.limitPerEvent ?? 1000;
10
+ this.tenantPropertyKey = options.tenantPropertyKey ?? "tenantId";
11
+ this.defaultRangeDays = options.defaultRangeDays ?? 30;
12
+ }
13
+ async getEventsForFunnel(definition, dateRange) {
14
+ const eventNames = definition.steps.map((step) => step.eventName);
15
+ return this.getEventsByNames(eventNames, dateRange);
16
+ }
17
+ async getEventsForCohort(definition, dateRange, eventNames) {
18
+ const events = eventNames && eventNames.length > 0 ? eventNames : ["*"];
19
+ if (events[0] === "*") {
20
+ return this.getEventsByNames([], dateRange, definition.periods * 500);
21
+ }
22
+ return this.getEventsByNames(events, dateRange, definition.periods * 500);
23
+ }
24
+ async getUserActivity(userId, dateRange, limit = 1000) {
25
+ if (!this.reader.getEvents) {
26
+ throw new Error("Analytics reader does not support event queries.");
27
+ }
28
+ const response = await this.reader.getEvents({
29
+ distinctId: userId,
30
+ dateRange,
31
+ limit
32
+ });
33
+ return response.results.map((event) => toAnalyticsEvent(event, this.tenantPropertyKey));
34
+ }
35
+ async getGrowthMetrics(metricNames, dateRange) {
36
+ const range = resolveRange(dateRange, this.defaultRangeDays);
37
+ const previous = shiftRange(range);
38
+ const results = [];
39
+ for (const metricName of metricNames) {
40
+ const current = await this.countEvents(metricName, range);
41
+ const previousCount = await this.countEvents(metricName, previous);
42
+ results.push({
43
+ name: metricName,
44
+ current,
45
+ previous: previousCount
46
+ });
47
+ }
48
+ return results;
49
+ }
50
+ async getEventsByNames(eventNames, dateRange, limitPerEvent = this.limitPerEvent) {
51
+ if (!this.reader.getEvents) {
52
+ throw new Error("Analytics reader does not support event queries.");
53
+ }
54
+ if (eventNames.length === 0) {
55
+ const response = await this.reader.getEvents({
56
+ dateRange,
57
+ limit: limitPerEvent
58
+ });
59
+ return response.results.map((event) => toAnalyticsEvent(event, this.tenantPropertyKey));
60
+ }
61
+ const events = [];
62
+ for (const eventName of eventNames) {
63
+ const response = await this.reader.getEvents({
64
+ event: eventName,
65
+ dateRange,
66
+ limit: limitPerEvent
67
+ });
68
+ response.results.forEach((event) => {
69
+ events.push(toAnalyticsEvent(event, this.tenantPropertyKey));
70
+ });
71
+ }
72
+ return events;
73
+ }
74
+ async countEvents(eventName, range) {
75
+ if (!this.reader.queryHogQL) {
76
+ throw new Error("Analytics reader does not support HogQL queries.");
77
+ }
78
+ const result = await this.reader.queryHogQL({
79
+ query: [
80
+ "select",
81
+ " count() as total",
82
+ "from events",
83
+ "where event = {eventName}",
84
+ "and timestamp >= {dateFrom}",
85
+ "and timestamp < {dateTo}"
86
+ ].join(`
87
+ `),
88
+ values: {
89
+ eventName,
90
+ dateFrom: range.from.toISOString(),
91
+ dateTo: range.to.toISOString()
92
+ }
93
+ });
94
+ return readSingleNumber(result) ?? 0;
95
+ }
96
+ }
97
+ function toAnalyticsEvent(event, tenantPropertyKey) {
98
+ const tenantIdValue = event.properties?.[tenantPropertyKey] ?? event.properties?.tenantId;
99
+ return {
100
+ name: event.event,
101
+ userId: event.distinctId,
102
+ tenantId: typeof tenantIdValue === "string" ? tenantIdValue : undefined,
103
+ timestamp: event.timestamp,
104
+ properties: event.properties
105
+ };
106
+ }
107
+ function resolveRange(dateRange, defaultDays) {
108
+ const to = dateRange?.to instanceof Date ? dateRange.to : dateRange?.to ? new Date(dateRange.to) : new Date;
109
+ const from = dateRange?.from instanceof Date ? dateRange.from : dateRange?.from ? new Date(dateRange.from) : new Date(to.getTime() - defaultDays * 24 * 60 * 60 * 1000);
110
+ return { from, to };
111
+ }
112
+ function shiftRange(range) {
113
+ const duration = range.to.getTime() - range.from.getTime();
114
+ return {
115
+ from: new Date(range.from.getTime() - duration),
116
+ to: new Date(range.to.getTime() - duration)
117
+ };
118
+ }
119
+ function readSingleNumber(result) {
120
+ if (!Array.isArray(result.results) || result.results.length === 0) {
121
+ return;
122
+ }
123
+ const firstRow = result.results[0];
124
+ if (Array.isArray(firstRow) && firstRow.length > 0) {
125
+ const value = firstRow[0];
126
+ if (typeof value === "number" && Number.isFinite(value))
127
+ return value;
128
+ if (typeof value === "string" && value.trim()) {
129
+ const parsed = Number(value);
130
+ if (Number.isFinite(parsed))
131
+ return parsed;
132
+ }
133
+ }
134
+ return;
135
+ }
136
+ export {
137
+ PosthogAnalyticsEventSource
138
+ };
@@ -0,0 +1,138 @@
1
+ // src/posthog/event-source.ts
2
+ class PosthogAnalyticsEventSource {
3
+ reader;
4
+ limitPerEvent;
5
+ tenantPropertyKey;
6
+ defaultRangeDays;
7
+ constructor(reader, options = {}) {
8
+ this.reader = reader;
9
+ this.limitPerEvent = options.limitPerEvent ?? 1000;
10
+ this.tenantPropertyKey = options.tenantPropertyKey ?? "tenantId";
11
+ this.defaultRangeDays = options.defaultRangeDays ?? 30;
12
+ }
13
+ async getEventsForFunnel(definition, dateRange) {
14
+ const eventNames = definition.steps.map((step) => step.eventName);
15
+ return this.getEventsByNames(eventNames, dateRange);
16
+ }
17
+ async getEventsForCohort(definition, dateRange, eventNames) {
18
+ const events = eventNames && eventNames.length > 0 ? eventNames : ["*"];
19
+ if (events[0] === "*") {
20
+ return this.getEventsByNames([], dateRange, definition.periods * 500);
21
+ }
22
+ return this.getEventsByNames(events, dateRange, definition.periods * 500);
23
+ }
24
+ async getUserActivity(userId, dateRange, limit = 1000) {
25
+ if (!this.reader.getEvents) {
26
+ throw new Error("Analytics reader does not support event queries.");
27
+ }
28
+ const response = await this.reader.getEvents({
29
+ distinctId: userId,
30
+ dateRange,
31
+ limit
32
+ });
33
+ return response.results.map((event) => toAnalyticsEvent(event, this.tenantPropertyKey));
34
+ }
35
+ async getGrowthMetrics(metricNames, dateRange) {
36
+ const range = resolveRange(dateRange, this.defaultRangeDays);
37
+ const previous = shiftRange(range);
38
+ const results = [];
39
+ for (const metricName of metricNames) {
40
+ const current = await this.countEvents(metricName, range);
41
+ const previousCount = await this.countEvents(metricName, previous);
42
+ results.push({
43
+ name: metricName,
44
+ current,
45
+ previous: previousCount
46
+ });
47
+ }
48
+ return results;
49
+ }
50
+ async getEventsByNames(eventNames, dateRange, limitPerEvent = this.limitPerEvent) {
51
+ if (!this.reader.getEvents) {
52
+ throw new Error("Analytics reader does not support event queries.");
53
+ }
54
+ if (eventNames.length === 0) {
55
+ const response = await this.reader.getEvents({
56
+ dateRange,
57
+ limit: limitPerEvent
58
+ });
59
+ return response.results.map((event) => toAnalyticsEvent(event, this.tenantPropertyKey));
60
+ }
61
+ const events = [];
62
+ for (const eventName of eventNames) {
63
+ const response = await this.reader.getEvents({
64
+ event: eventName,
65
+ dateRange,
66
+ limit: limitPerEvent
67
+ });
68
+ response.results.forEach((event) => {
69
+ events.push(toAnalyticsEvent(event, this.tenantPropertyKey));
70
+ });
71
+ }
72
+ return events;
73
+ }
74
+ async countEvents(eventName, range) {
75
+ if (!this.reader.queryHogQL) {
76
+ throw new Error("Analytics reader does not support HogQL queries.");
77
+ }
78
+ const result = await this.reader.queryHogQL({
79
+ query: [
80
+ "select",
81
+ " count() as total",
82
+ "from events",
83
+ "where event = {eventName}",
84
+ "and timestamp >= {dateFrom}",
85
+ "and timestamp < {dateTo}"
86
+ ].join(`
87
+ `),
88
+ values: {
89
+ eventName,
90
+ dateFrom: range.from.toISOString(),
91
+ dateTo: range.to.toISOString()
92
+ }
93
+ });
94
+ return readSingleNumber(result) ?? 0;
95
+ }
96
+ }
97
+ function toAnalyticsEvent(event, tenantPropertyKey) {
98
+ const tenantIdValue = event.properties?.[tenantPropertyKey] ?? event.properties?.tenantId;
99
+ return {
100
+ name: event.event,
101
+ userId: event.distinctId,
102
+ tenantId: typeof tenantIdValue === "string" ? tenantIdValue : undefined,
103
+ timestamp: event.timestamp,
104
+ properties: event.properties
105
+ };
106
+ }
107
+ function resolveRange(dateRange, defaultDays) {
108
+ const to = dateRange?.to instanceof Date ? dateRange.to : dateRange?.to ? new Date(dateRange.to) : new Date;
109
+ const from = dateRange?.from instanceof Date ? dateRange.from : dateRange?.from ? new Date(dateRange.from) : new Date(to.getTime() - defaultDays * 24 * 60 * 60 * 1000);
110
+ return { from, to };
111
+ }
112
+ function shiftRange(range) {
113
+ const duration = range.to.getTime() - range.from.getTime();
114
+ return {
115
+ from: new Date(range.from.getTime() - duration),
116
+ to: new Date(range.to.getTime() - duration)
117
+ };
118
+ }
119
+ function readSingleNumber(result) {
120
+ if (!Array.isArray(result.results) || result.results.length === 0) {
121
+ return;
122
+ }
123
+ const firstRow = result.results[0];
124
+ if (Array.isArray(firstRow) && firstRow.length > 0) {
125
+ const value = firstRow[0];
126
+ if (typeof value === "number" && Number.isFinite(value))
127
+ return value;
128
+ if (typeof value === "string" && value.trim()) {
129
+ const parsed = Number(value);
130
+ if (Number.isFinite(parsed))
131
+ return parsed;
132
+ }
133
+ }
134
+ return;
135
+ }
136
+ export {
137
+ PosthogAnalyticsEventSource
138
+ };
File without changes
@@ -1,2 +1,2 @@
1
- import { ChurnPredictor, ChurnPredictorOptions } from "./predictor.js";
2
- export { ChurnPredictor, ChurnPredictorOptions };
1
+ export * from './predictor';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/churn/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC"}
@@ -1,3 +1,78 @@
1
- import { ChurnPredictor } from "./predictor.js";
1
+ // @bun
2
+ // src/churn/predictor.ts
3
+ import dayjs from "dayjs";
2
4
 
3
- export { ChurnPredictor };
5
+ class ChurnPredictor {
6
+ recencyWeight;
7
+ frequencyWeight;
8
+ errorWeight;
9
+ decayDays;
10
+ constructor(options) {
11
+ this.recencyWeight = options?.recencyWeight ?? 0.5;
12
+ this.frequencyWeight = options?.frequencyWeight ?? 0.3;
13
+ this.errorWeight = options?.errorWeight ?? 0.2;
14
+ this.decayDays = options?.decayDays ?? 14;
15
+ }
16
+ score(events) {
17
+ const grouped = groupBy(events, (event) => event.userId);
18
+ const signals = [];
19
+ for (const [userId, userEvents] of grouped.entries()) {
20
+ const score = this.computeScore(userEvents);
21
+ signals.push({
22
+ userId,
23
+ score,
24
+ bucket: score >= 0.7 ? "high" : score >= 0.4 ? "medium" : "low",
25
+ drivers: this.drivers(userEvents)
26
+ });
27
+ }
28
+ return signals.sort((a, b) => b.score - a.score);
29
+ }
30
+ computeScore(events) {
31
+ if (!events.length)
32
+ return 0;
33
+ const sorted = events.sort((a, b) => dateMs(a) - dateMs(b));
34
+ const lastEvent = sorted[sorted.length - 1];
35
+ if (!lastEvent)
36
+ return 0;
37
+ const daysSinceLast = dayjs().diff(dayjs(lastEvent.timestamp), "day");
38
+ const recencyScore = Math.max(0, 1 - daysSinceLast / this.decayDays);
39
+ const windowStart = dayjs().subtract(this.decayDays, "day");
40
+ const recentEvents = sorted.filter((event) => dayjs(event.timestamp).isAfter(windowStart));
41
+ const averagePerDay = recentEvents.length / Math.max(this.decayDays, 1);
42
+ const frequencyScore = Math.min(1, averagePerDay * 5);
43
+ const errorEvents = recentEvents.filter((event) => typeof event.properties?.error !== "undefined" || /error|failed/i.test(event.name)).length;
44
+ const errorScore = Math.min(1, errorEvents / 3);
45
+ const score = recencyScore * this.recencyWeight + frequencyScore * this.frequencyWeight + (1 - errorScore) * this.errorWeight;
46
+ return Number(score.toFixed(3));
47
+ }
48
+ drivers(events) {
49
+ const drivers = [];
50
+ const sorted = events.sort((a, b) => dateMs(a) - dateMs(b));
51
+ const lastEvent = sorted[sorted.length - 1];
52
+ if (lastEvent) {
53
+ const days = dayjs().diff(dayjs(lastEvent.timestamp), "day");
54
+ if (days > this.decayDays)
55
+ drivers.push(`Inactive for ${days} days`);
56
+ }
57
+ const errorEvents = events.filter((event) => typeof event.properties?.error !== "undefined" || /error|failed/i.test(event.name));
58
+ if (errorEvents.length)
59
+ drivers.push(`${errorEvents.length} errors logged`);
60
+ return drivers;
61
+ }
62
+ }
63
+ function groupBy(items, selector) {
64
+ const map = new Map;
65
+ for (const item of items) {
66
+ const key = selector(item);
67
+ const list = map.get(key) ?? [];
68
+ list.push(item);
69
+ map.set(key, list);
70
+ }
71
+ return map;
72
+ }
73
+ function dateMs(event) {
74
+ return new Date(event.timestamp).getTime();
75
+ }
76
+ export {
77
+ ChurnPredictor
78
+ };
@@ -1,22 +1,18 @@
1
- import { AnalyticsEvent, ChurnSignal } from "../types.js";
2
-
3
- //#region src/churn/predictor.d.ts
4
- interface ChurnPredictorOptions {
5
- recencyWeight?: number;
6
- frequencyWeight?: number;
7
- errorWeight?: number;
8
- decayDays?: number;
1
+ import type { AnalyticsEvent, ChurnSignal } from '../types';
2
+ export interface ChurnPredictorOptions {
3
+ recencyWeight?: number;
4
+ frequencyWeight?: number;
5
+ errorWeight?: number;
6
+ decayDays?: number;
9
7
  }
10
- declare class ChurnPredictor {
11
- private readonly recencyWeight;
12
- private readonly frequencyWeight;
13
- private readonly errorWeight;
14
- private readonly decayDays;
15
- constructor(options?: ChurnPredictorOptions);
16
- score(events: AnalyticsEvent[]): ChurnSignal[];
17
- private computeScore;
18
- private drivers;
8
+ export declare class ChurnPredictor {
9
+ private readonly recencyWeight;
10
+ private readonly frequencyWeight;
11
+ private readonly errorWeight;
12
+ private readonly decayDays;
13
+ constructor(options?: ChurnPredictorOptions);
14
+ score(events: AnalyticsEvent[]): ChurnSignal[];
15
+ private computeScore;
16
+ private drivers;
19
17
  }
20
- //#endregion
21
- export { ChurnPredictor, ChurnPredictorOptions };
22
18
  //# sourceMappingURL=predictor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"predictor.d.ts","names":[],"sources":["../../src/churn/predictor.ts"],"mappings":";;;UAGiB,qBAAA;EACf,aAAA;EACA,eAAA;EACA,WAAA;EACA,SAAA;AAAA;AAAA,cAGW,cAAA;EAAA,iBACM,aAAA;EAAA,iBACA,eAAA;EAAA,iBACA,WAAA;EAAA,iBACA,SAAA;cAEL,OAAA,GAAU,qBAAA;EAOtB,KAAA,CAAM,MAAA,EAAQ,cAAA,KAAmB,WAAA;EAAA,QAezB,YAAA;EAAA,QA6BA,OAAA;AAAA"}
1
+ {"version":3,"file":"predictor.d.ts","sourceRoot":"","sources":["../../src/churn/predictor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5D,MAAM,WAAW,qBAAqB;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,OAAO,CAAC,EAAE,qBAAqB;IAO3C,KAAK,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,WAAW,EAAE;IAe9C,OAAO,CAAC,YAAY;IA6BpB,OAAO,CAAC,OAAO;CAgBhB"}
@@ -1,74 +1,78 @@
1
+ // @bun
2
+ // src/churn/predictor.ts
1
3
  import dayjs from "dayjs";
2
4
 
3
- //#region src/churn/predictor.ts
4
- var ChurnPredictor = class {
5
- recencyWeight;
6
- frequencyWeight;
7
- errorWeight;
8
- decayDays;
9
- constructor(options) {
10
- this.recencyWeight = options?.recencyWeight ?? .5;
11
- this.frequencyWeight = options?.frequencyWeight ?? .3;
12
- this.errorWeight = options?.errorWeight ?? .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 >= .7 ? "high" : score >= .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) return 0;
31
- const sorted = events.sort((a, b) => dateMs(a) - dateMs(b));
32
- const lastEvent = sorted[sorted.length - 1];
33
- if (!lastEvent) return 0;
34
- const daysSinceLast = dayjs().diff(dayjs(lastEvent.timestamp), "day");
35
- const recencyScore = Math.max(0, 1 - daysSinceLast / this.decayDays);
36
- const windowStart = dayjs().subtract(this.decayDays, "day");
37
- const recentEvents = sorted.filter((event) => dayjs(event.timestamp).isAfter(windowStart));
38
- const averagePerDay = recentEvents.length / Math.max(this.decayDays, 1);
39
- const frequencyScore = Math.min(1, averagePerDay * 5);
40
- const errorEvents = recentEvents.filter((event) => typeof event.properties?.error !== "undefined" || /error|failed/i.test(event.name)).length;
41
- const errorScore = Math.min(1, errorEvents / 3);
42
- const score = recencyScore * this.recencyWeight + frequencyScore * this.frequencyWeight + (1 - errorScore) * this.errorWeight;
43
- return Number(score.toFixed(3));
44
- }
45
- drivers(events) {
46
- const drivers = [];
47
- const sorted = events.sort((a, b) => dateMs(a) - dateMs(b));
48
- const lastEvent = sorted[sorted.length - 1];
49
- if (lastEvent) {
50
- const days = dayjs().diff(dayjs(lastEvent.timestamp), "day");
51
- if (days > this.decayDays) drivers.push(`Inactive for ${days} days`);
52
- }
53
- const errorEvents = events.filter((event) => typeof event.properties?.error !== "undefined" || /error|failed/i.test(event.name));
54
- if (errorEvents.length) drivers.push(`${errorEvents.length} errors logged`);
55
- return drivers;
56
- }
57
- };
5
+ class ChurnPredictor {
6
+ recencyWeight;
7
+ frequencyWeight;
8
+ errorWeight;
9
+ decayDays;
10
+ constructor(options) {
11
+ this.recencyWeight = options?.recencyWeight ?? 0.5;
12
+ this.frequencyWeight = options?.frequencyWeight ?? 0.3;
13
+ this.errorWeight = options?.errorWeight ?? 0.2;
14
+ this.decayDays = options?.decayDays ?? 14;
15
+ }
16
+ score(events) {
17
+ const grouped = groupBy(events, (event) => event.userId);
18
+ const signals = [];
19
+ for (const [userId, userEvents] of grouped.entries()) {
20
+ const score = this.computeScore(userEvents);
21
+ signals.push({
22
+ userId,
23
+ score,
24
+ bucket: score >= 0.7 ? "high" : score >= 0.4 ? "medium" : "low",
25
+ drivers: this.drivers(userEvents)
26
+ });
27
+ }
28
+ return signals.sort((a, b) => b.score - a.score);
29
+ }
30
+ computeScore(events) {
31
+ if (!events.length)
32
+ return 0;
33
+ const sorted = events.sort((a, b) => dateMs(a) - dateMs(b));
34
+ const lastEvent = sorted[sorted.length - 1];
35
+ if (!lastEvent)
36
+ return 0;
37
+ const daysSinceLast = dayjs().diff(dayjs(lastEvent.timestamp), "day");
38
+ const recencyScore = Math.max(0, 1 - daysSinceLast / this.decayDays);
39
+ const windowStart = dayjs().subtract(this.decayDays, "day");
40
+ const recentEvents = sorted.filter((event) => dayjs(event.timestamp).isAfter(windowStart));
41
+ const averagePerDay = recentEvents.length / Math.max(this.decayDays, 1);
42
+ const frequencyScore = Math.min(1, averagePerDay * 5);
43
+ const errorEvents = recentEvents.filter((event) => typeof event.properties?.error !== "undefined" || /error|failed/i.test(event.name)).length;
44
+ const errorScore = Math.min(1, errorEvents / 3);
45
+ const score = recencyScore * this.recencyWeight + frequencyScore * this.frequencyWeight + (1 - errorScore) * this.errorWeight;
46
+ return Number(score.toFixed(3));
47
+ }
48
+ drivers(events) {
49
+ const drivers = [];
50
+ const sorted = events.sort((a, b) => dateMs(a) - dateMs(b));
51
+ const lastEvent = sorted[sorted.length - 1];
52
+ if (lastEvent) {
53
+ const days = dayjs().diff(dayjs(lastEvent.timestamp), "day");
54
+ if (days > this.decayDays)
55
+ drivers.push(`Inactive for ${days} days`);
56
+ }
57
+ const errorEvents = events.filter((event) => typeof event.properties?.error !== "undefined" || /error|failed/i.test(event.name));
58
+ if (errorEvents.length)
59
+ drivers.push(`${errorEvents.length} errors logged`);
60
+ return drivers;
61
+ }
62
+ }
58
63
  function groupBy(items, selector) {
59
- const map = /* @__PURE__ */ new Map();
60
- for (const item of items) {
61
- const key = selector(item);
62
- const list = map.get(key) ?? [];
63
- list.push(item);
64
- map.set(key, list);
65
- }
66
- return map;
64
+ const map = new Map;
65
+ for (const item of items) {
66
+ const key = selector(item);
67
+ const list = map.get(key) ?? [];
68
+ list.push(item);
69
+ map.set(key, list);
70
+ }
71
+ return map;
67
72
  }
68
73
  function dateMs(event) {
69
- return new Date(event.timestamp).getTime();
74
+ return new Date(event.timestamp).getTime();
70
75
  }
71
-
72
- //#endregion
73
- export { ChurnPredictor };
74
- //# sourceMappingURL=predictor.js.map
76
+ export {
77
+ ChurnPredictor
78
+ };
@@ -1,2 +1,2 @@
1
- import { CohortTracker } from "./tracker.js";
2
- export { CohortTracker };
1
+ export * from './tracker';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cohort/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC"}