@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,3 +1,118 @@
1
- import { CohortTracker } from "./tracker.js";
1
+ // @bun
2
+ // src/cohort/tracker.ts
3
+ import dayjs from "dayjs";
2
4
 
3
- export { CohortTracker };
5
+ class CohortTracker {
6
+ analyze(events, definition) {
7
+ const groupedByUser = groupBy(events, (event) => event.userId);
8
+ const cohorts = new Map;
9
+ for (const [userId, userEvents] of groupedByUser.entries()) {
10
+ userEvents.sort((a, b) => dateMs(a) - dateMs(b));
11
+ const signup = userEvents[0];
12
+ if (!signup)
13
+ continue;
14
+ const cohortKey = bucketKey(signup.timestamp, definition.bucket);
15
+ const builder = cohorts.get(cohortKey) ?? new CohortStatsBuilder(cohortKey, definition);
16
+ builder.addUser(userId);
17
+ for (const event of userEvents) {
18
+ builder.addEvent(userId, event);
19
+ }
20
+ cohorts.set(cohortKey, builder);
21
+ }
22
+ return {
23
+ definition,
24
+ cohorts: [...cohorts.values()].map((builder) => builder.build())
25
+ };
26
+ }
27
+ }
28
+
29
+ class CohortStatsBuilder {
30
+ key;
31
+ definition;
32
+ users = new Set;
33
+ retentionMap = new Map;
34
+ ltv = 0;
35
+ constructor(key, definition) {
36
+ this.key = key;
37
+ this.definition = definition;
38
+ }
39
+ addUser(userId) {
40
+ this.users.add(userId);
41
+ }
42
+ addEvent(userId, event) {
43
+ const period = bucketDiff(this.key, event.timestamp, this.definition.bucket);
44
+ if (period < 0 || period >= this.definition.periods)
45
+ return;
46
+ const bucket = this.retentionMap.get(period) ?? new Set;
47
+ bucket.add(userId);
48
+ this.retentionMap.set(period, bucket);
49
+ const amount = typeof event.properties?.amount === "number" ? event.properties.amount : 0;
50
+ this.ltv += amount;
51
+ }
52
+ build() {
53
+ const totalUsers = this.users.size || 1;
54
+ const retention = [];
55
+ for (let period = 0;period < this.definition.periods; period++) {
56
+ const active = this.retentionMap.get(period)?.size ?? 0;
57
+ retention.push(Number((active / totalUsers).toFixed(3)));
58
+ }
59
+ return {
60
+ cohortKey: this.key,
61
+ users: this.users.size,
62
+ retention,
63
+ ltv: Number(this.ltv.toFixed(2))
64
+ };
65
+ }
66
+ }
67
+ function groupBy(items, selector) {
68
+ const map = new Map;
69
+ for (const item of items) {
70
+ const key = selector(item);
71
+ const list = map.get(key) ?? [];
72
+ list.push(item);
73
+ map.set(key, list);
74
+ }
75
+ return map;
76
+ }
77
+ function bucketKey(timestamp, bucket) {
78
+ const dt = dayjs(timestamp);
79
+ switch (bucket) {
80
+ case "day":
81
+ return dt.startOf("day").format("YYYY-MM-DD");
82
+ case "week":
83
+ return dt.startOf("week").format("YYYY-[W]WW");
84
+ case "month":
85
+ default:
86
+ return dt.startOf("month").format("YYYY-MM");
87
+ }
88
+ }
89
+ function bucketDiff(cohortKey, timestamp, bucket) {
90
+ const start = parseBucketKey(cohortKey, bucket);
91
+ const target = dayjs(timestamp);
92
+ switch (bucket) {
93
+ case "day":
94
+ return target.diff(start, "day");
95
+ case "week":
96
+ return target.diff(start, "week");
97
+ case "month":
98
+ default:
99
+ return target.diff(start, "month");
100
+ }
101
+ }
102
+ function parseBucketKey(key, bucket) {
103
+ switch (bucket) {
104
+ case "day":
105
+ return dayjs(key, "YYYY-MM-DD");
106
+ case "week":
107
+ return dayjs(key.replace("W", ""), "YYYY-ww");
108
+ case "month":
109
+ default:
110
+ return dayjs(key, "YYYY-MM");
111
+ }
112
+ }
113
+ function dateMs(event) {
114
+ return new Date(event.timestamp).getTime();
115
+ }
116
+ export {
117
+ CohortTracker
118
+ };
@@ -1,9 +1,5 @@
1
- import { AnalyticsEvent, CohortAnalysis, CohortDefinition } from "../types.js";
2
-
3
- //#region src/cohort/tracker.d.ts
4
- declare class CohortTracker {
5
- analyze(events: AnalyticsEvent[], definition: CohortDefinition): CohortAnalysis;
1
+ import type { AnalyticsEvent, CohortAnalysis, CohortDefinition } from '../types';
2
+ export declare class CohortTracker {
3
+ analyze(events: AnalyticsEvent[], definition: CohortDefinition): CohortAnalysis;
6
4
  }
7
- //#endregion
8
- export { CohortTracker };
9
5
  //# sourceMappingURL=tracker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tracker.d.ts","names":[],"sources":["../../src/cohort/tracker.ts"],"mappings":";;;cAQa,aAAA;EACX,OAAA,CACE,MAAA,EAAQ,cAAA,IACR,UAAA,EAAY,gBAAA,GACX,cAAA;AAAA"}
1
+ {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/cohort/tracker.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,gBAAgB,EAEjB,MAAM,UAAU,CAAC;AAElB,qBAAa,aAAa;IACxB,OAAO,CACL,MAAM,EAAE,cAAc,EAAE,EACxB,UAAU,EAAE,gBAAgB,GAC3B,cAAc;CAuBlB"}
@@ -1,99 +1,118 @@
1
+ // @bun
2
+ // src/cohort/tracker.ts
1
3
  import dayjs from "dayjs";
2
4
 
3
- //#region src/cohort/tracker.ts
4
- var CohortTracker = class {
5
- analyze(events, definition) {
6
- const groupedByUser = groupBy(events, (event) => event.userId);
7
- const cohorts = /* @__PURE__ */ new Map();
8
- for (const [userId, userEvents] of groupedByUser.entries()) {
9
- userEvents.sort((a, b) => dateMs(a) - dateMs(b));
10
- const signup = userEvents[0];
11
- if (!signup) continue;
12
- const cohortKey = bucketKey(signup.timestamp, definition.bucket);
13
- const builder = cohorts.get(cohortKey) ?? new CohortStatsBuilder(cohortKey, definition);
14
- builder.addUser(userId);
15
- for (const event of userEvents) builder.addEvent(userId, event);
16
- cohorts.set(cohortKey, builder);
17
- }
18
- return {
19
- definition,
20
- cohorts: [...cohorts.values()].map((builder) => builder.build())
21
- };
22
- }
23
- };
24
- var CohortStatsBuilder = class {
25
- users = /* @__PURE__ */ new Set();
26
- retentionMap = /* @__PURE__ */ new Map();
27
- ltv = 0;
28
- constructor(key, definition) {
29
- this.key = key;
30
- this.definition = definition;
31
- }
32
- addUser(userId) {
33
- this.users.add(userId);
34
- }
35
- addEvent(userId, event) {
36
- const period = bucketDiff(this.key, event.timestamp, this.definition.bucket);
37
- if (period < 0 || period >= this.definition.periods) return;
38
- const bucket = this.retentionMap.get(period) ?? /* @__PURE__ */ new Set();
39
- bucket.add(userId);
40
- this.retentionMap.set(period, bucket);
41
- const amount = typeof event.properties?.amount === "number" ? event.properties.amount : 0;
42
- this.ltv += amount;
43
- }
44
- build() {
45
- const totalUsers = this.users.size || 1;
46
- const retention = [];
47
- for (let period = 0; period < this.definition.periods; period++) {
48
- const active = this.retentionMap.get(period)?.size ?? 0;
49
- retention.push(Number((active / totalUsers).toFixed(3)));
50
- }
51
- return {
52
- cohortKey: this.key,
53
- users: this.users.size,
54
- retention,
55
- ltv: Number(this.ltv.toFixed(2))
56
- };
57
- }
58
- };
5
+ class CohortTracker {
6
+ analyze(events, definition) {
7
+ const groupedByUser = groupBy(events, (event) => event.userId);
8
+ const cohorts = new Map;
9
+ for (const [userId, userEvents] of groupedByUser.entries()) {
10
+ userEvents.sort((a, b) => dateMs(a) - dateMs(b));
11
+ const signup = userEvents[0];
12
+ if (!signup)
13
+ continue;
14
+ const cohortKey = bucketKey(signup.timestamp, definition.bucket);
15
+ const builder = cohorts.get(cohortKey) ?? new CohortStatsBuilder(cohortKey, definition);
16
+ builder.addUser(userId);
17
+ for (const event of userEvents) {
18
+ builder.addEvent(userId, event);
19
+ }
20
+ cohorts.set(cohortKey, builder);
21
+ }
22
+ return {
23
+ definition,
24
+ cohorts: [...cohorts.values()].map((builder) => builder.build())
25
+ };
26
+ }
27
+ }
28
+
29
+ class CohortStatsBuilder {
30
+ key;
31
+ definition;
32
+ users = new Set;
33
+ retentionMap = new Map;
34
+ ltv = 0;
35
+ constructor(key, definition) {
36
+ this.key = key;
37
+ this.definition = definition;
38
+ }
39
+ addUser(userId) {
40
+ this.users.add(userId);
41
+ }
42
+ addEvent(userId, event) {
43
+ const period = bucketDiff(this.key, event.timestamp, this.definition.bucket);
44
+ if (period < 0 || period >= this.definition.periods)
45
+ return;
46
+ const bucket = this.retentionMap.get(period) ?? new Set;
47
+ bucket.add(userId);
48
+ this.retentionMap.set(period, bucket);
49
+ const amount = typeof event.properties?.amount === "number" ? event.properties.amount : 0;
50
+ this.ltv += amount;
51
+ }
52
+ build() {
53
+ const totalUsers = this.users.size || 1;
54
+ const retention = [];
55
+ for (let period = 0;period < this.definition.periods; period++) {
56
+ const active = this.retentionMap.get(period)?.size ?? 0;
57
+ retention.push(Number((active / totalUsers).toFixed(3)));
58
+ }
59
+ return {
60
+ cohortKey: this.key,
61
+ users: this.users.size,
62
+ retention,
63
+ ltv: Number(this.ltv.toFixed(2))
64
+ };
65
+ }
66
+ }
59
67
  function groupBy(items, selector) {
60
- const map = /* @__PURE__ */ new Map();
61
- for (const item of items) {
62
- const key = selector(item);
63
- const list = map.get(key) ?? [];
64
- list.push(item);
65
- map.set(key, list);
66
- }
67
- return map;
68
+ const map = new Map;
69
+ for (const item of items) {
70
+ const key = selector(item);
71
+ const list = map.get(key) ?? [];
72
+ list.push(item);
73
+ map.set(key, list);
74
+ }
75
+ return map;
68
76
  }
69
77
  function bucketKey(timestamp, bucket) {
70
- const dt = dayjs(timestamp);
71
- switch (bucket) {
72
- case "day": return dt.startOf("day").format("YYYY-MM-DD");
73
- case "week": return dt.startOf("week").format("YYYY-[W]WW");
74
- default: return dt.startOf("month").format("YYYY-MM");
75
- }
78
+ const dt = dayjs(timestamp);
79
+ switch (bucket) {
80
+ case "day":
81
+ return dt.startOf("day").format("YYYY-MM-DD");
82
+ case "week":
83
+ return dt.startOf("week").format("YYYY-[W]WW");
84
+ case "month":
85
+ default:
86
+ return dt.startOf("month").format("YYYY-MM");
87
+ }
76
88
  }
77
89
  function bucketDiff(cohortKey, timestamp, bucket) {
78
- const start = parseBucketKey(cohortKey, bucket);
79
- const target = dayjs(timestamp);
80
- switch (bucket) {
81
- case "day": return target.diff(start, "day");
82
- case "week": return target.diff(start, "week");
83
- default: return target.diff(start, "month");
84
- }
90
+ const start = parseBucketKey(cohortKey, bucket);
91
+ const target = dayjs(timestamp);
92
+ switch (bucket) {
93
+ case "day":
94
+ return target.diff(start, "day");
95
+ case "week":
96
+ return target.diff(start, "week");
97
+ case "month":
98
+ default:
99
+ return target.diff(start, "month");
100
+ }
85
101
  }
86
102
  function parseBucketKey(key, bucket) {
87
- switch (bucket) {
88
- case "day": return dayjs(key, "YYYY-MM-DD");
89
- case "week": return dayjs(key.replace("W", ""), "YYYY-ww");
90
- default: return dayjs(key, "YYYY-MM");
91
- }
103
+ switch (bucket) {
104
+ case "day":
105
+ return dayjs(key, "YYYY-MM-DD");
106
+ case "week":
107
+ return dayjs(key.replace("W", ""), "YYYY-ww");
108
+ case "month":
109
+ default:
110
+ return dayjs(key, "YYYY-MM");
111
+ }
92
112
  }
93
113
  function dateMs(event) {
94
- return new Date(event.timestamp).getTime();
114
+ return new Date(event.timestamp).getTime();
95
115
  }
96
-
97
- //#endregion
98
- export { CohortTracker };
99
- //# sourceMappingURL=tracker.js.map
116
+ export {
117
+ CohortTracker
118
+ };
@@ -1,10 +1,6 @@
1
- import { AnalyticsEvent, FunnelAnalysis, FunnelDefinition } from "../types.js";
2
-
3
- //#region src/funnel/analyzer.d.ts
4
- declare class FunnelAnalyzer {
5
- analyze(events: AnalyticsEvent[], definition: FunnelDefinition): FunnelAnalysis;
6
- private evaluateUser;
1
+ import type { AnalyticsEvent, FunnelAnalysis, FunnelDefinition } from '../types';
2
+ export declare class FunnelAnalyzer {
3
+ analyze(events: AnalyticsEvent[], definition: FunnelDefinition): FunnelAnalysis;
4
+ private evaluateUser;
7
5
  }
8
- //#endregion
9
- export { FunnelAnalyzer };
10
6
  //# sourceMappingURL=analyzer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","names":[],"sources":["../../src/funnel/analyzer.ts"],"mappings":";;;cAQa,cAAA;EACX,OAAA,CACE,MAAA,EAAQ,cAAA,IACR,UAAA,EAAY,gBAAA,GACX,cAAA;EAAA,QAmCK,YAAA;AAAA"}
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../../src/funnel/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,gBAAgB,EAGjB,MAAM,UAAU,CAAC;AAElB,qBAAa,cAAc;IACzB,OAAO,CACL,MAAM,EAAE,cAAc,EAAE,EACxB,UAAU,EAAE,gBAAgB,GAC3B,cAAc;IAmCjB,OAAO,CAAC,YAAY;CAmCrB"}
@@ -1,64 +1,69 @@
1
- //#region src/funnel/analyzer.ts
2
- var FunnelAnalyzer = class {
3
- analyze(events, definition) {
4
- const windowMs = (definition.windowHours ?? 72) * 60 * 60 * 1e3;
5
- const eventsByUser = groupByUser(events);
6
- const stepCounts = definition.steps.map(() => 0);
7
- for (const userEvents of eventsByUser.values()) this.evaluateUser(userEvents, definition.steps, windowMs).forEach((hit, stepIdx) => {
8
- if (hit) stepCounts[stepIdx] = (stepCounts[stepIdx] ?? 0) + 1;
9
- });
10
- const totalUsers = eventsByUser.size;
11
- return {
12
- definition,
13
- totalUsers,
14
- steps: definition.steps.map((step, index) => {
15
- const prevCount = index === 0 ? totalUsers : stepCounts[index - 1] || 1;
16
- const count = stepCounts[index] ?? 0;
17
- const conversionRate = prevCount === 0 ? 0 : Number((count / prevCount).toFixed(3));
18
- return {
19
- step,
20
- count,
21
- conversionRate,
22
- dropOffRate: Number((1 - conversionRate).toFixed(3))
23
- };
24
- })
25
- };
26
- }
27
- evaluateUser(events, steps, windowMs) {
28
- const sorted = [...events].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
29
- const completion = Array(steps.length).fill(false);
30
- let cursor = 0;
31
- let anchorTime;
32
- for (const event of sorted) {
33
- const step = steps[cursor];
34
- if (!step) break;
35
- if (event.name !== step.eventName) continue;
36
- if (step.match && !step.match(event)) continue;
37
- const eventTime = new Date(event.timestamp).getTime();
38
- if (cursor === 0) {
39
- anchorTime = eventTime;
40
- completion[cursor] = true;
41
- cursor += 1;
42
- continue;
43
- }
44
- if (anchorTime && eventTime - anchorTime <= windowMs) {
45
- completion[cursor] = true;
46
- cursor += 1;
47
- }
48
- }
49
- return completion;
50
- }
51
- };
1
+ // @bun
2
+ // src/funnel/analyzer.ts
3
+ class FunnelAnalyzer {
4
+ analyze(events, definition) {
5
+ const windowMs = (definition.windowHours ?? 72) * 60 * 60 * 1000;
6
+ const eventsByUser = groupByUser(events);
7
+ const stepCounts = definition.steps.map(() => 0);
8
+ for (const userEvents of eventsByUser.values()) {
9
+ const completionIndex = this.evaluateUser(userEvents, definition.steps, windowMs);
10
+ completionIndex.forEach((hit, stepIdx) => {
11
+ if (hit) {
12
+ stepCounts[stepIdx] = (stepCounts[stepIdx] ?? 0) + 1;
13
+ }
14
+ });
15
+ }
16
+ const totalUsers = eventsByUser.size;
17
+ const steps = definition.steps.map((step, index) => {
18
+ const prevCount = index === 0 ? totalUsers : stepCounts[index - 1] || 1;
19
+ const count = stepCounts[index] ?? 0;
20
+ const conversionRate = prevCount === 0 ? 0 : Number((count / prevCount).toFixed(3));
21
+ const dropOffRate = Number((1 - conversionRate).toFixed(3));
22
+ return { step, count, conversionRate, dropOffRate };
23
+ });
24
+ return {
25
+ definition,
26
+ totalUsers,
27
+ steps
28
+ };
29
+ }
30
+ evaluateUser(events, steps, windowMs) {
31
+ const sorted = [...events].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
32
+ const completion = Array(steps.length).fill(false);
33
+ let cursor = 0;
34
+ let anchorTime;
35
+ for (const event of sorted) {
36
+ const step = steps[cursor];
37
+ if (!step)
38
+ break;
39
+ if (event.name !== step.eventName)
40
+ continue;
41
+ if (step.match && !step.match(event))
42
+ continue;
43
+ const eventTime = new Date(event.timestamp).getTime();
44
+ if (cursor === 0) {
45
+ anchorTime = eventTime;
46
+ completion[cursor] = true;
47
+ cursor += 1;
48
+ continue;
49
+ }
50
+ if (anchorTime && eventTime - anchorTime <= windowMs) {
51
+ completion[cursor] = true;
52
+ cursor += 1;
53
+ }
54
+ }
55
+ return completion;
56
+ }
57
+ }
52
58
  function groupByUser(events) {
53
- const map = /* @__PURE__ */ new Map();
54
- for (const event of events) {
55
- const list = map.get(event.userId) ?? [];
56
- list.push(event);
57
- map.set(event.userId, list);
58
- }
59
- return map;
59
+ const map = new Map;
60
+ for (const event of events) {
61
+ const list = map.get(event.userId) ?? [];
62
+ list.push(event);
63
+ map.set(event.userId, list);
64
+ }
65
+ return map;
60
66
  }
61
-
62
- //#endregion
63
- export { FunnelAnalyzer };
64
- //# sourceMappingURL=analyzer.js.map
67
+ export {
68
+ FunnelAnalyzer
69
+ };
@@ -1,2 +1,2 @@
1
- import { FunnelAnalyzer } from "./analyzer.js";
2
- export { FunnelAnalyzer };
1
+ export * from './analyzer';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/funnel/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
@@ -1,3 +1,69 @@
1
- import { FunnelAnalyzer } from "./analyzer.js";
2
-
3
- export { FunnelAnalyzer };
1
+ // @bun
2
+ // src/funnel/analyzer.ts
3
+ class FunnelAnalyzer {
4
+ analyze(events, definition) {
5
+ const windowMs = (definition.windowHours ?? 72) * 60 * 60 * 1000;
6
+ const eventsByUser = groupByUser(events);
7
+ const stepCounts = definition.steps.map(() => 0);
8
+ for (const userEvents of eventsByUser.values()) {
9
+ const completionIndex = this.evaluateUser(userEvents, definition.steps, windowMs);
10
+ completionIndex.forEach((hit, stepIdx) => {
11
+ if (hit) {
12
+ stepCounts[stepIdx] = (stepCounts[stepIdx] ?? 0) + 1;
13
+ }
14
+ });
15
+ }
16
+ const totalUsers = eventsByUser.size;
17
+ const steps = definition.steps.map((step, index) => {
18
+ const prevCount = index === 0 ? totalUsers : stepCounts[index - 1] || 1;
19
+ const count = stepCounts[index] ?? 0;
20
+ const conversionRate = prevCount === 0 ? 0 : Number((count / prevCount).toFixed(3));
21
+ const dropOffRate = Number((1 - conversionRate).toFixed(3));
22
+ return { step, count, conversionRate, dropOffRate };
23
+ });
24
+ return {
25
+ definition,
26
+ totalUsers,
27
+ steps
28
+ };
29
+ }
30
+ evaluateUser(events, steps, windowMs) {
31
+ const sorted = [...events].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
32
+ const completion = Array(steps.length).fill(false);
33
+ let cursor = 0;
34
+ let anchorTime;
35
+ for (const event of sorted) {
36
+ const step = steps[cursor];
37
+ if (!step)
38
+ break;
39
+ if (event.name !== step.eventName)
40
+ continue;
41
+ if (step.match && !step.match(event))
42
+ continue;
43
+ const eventTime = new Date(event.timestamp).getTime();
44
+ if (cursor === 0) {
45
+ anchorTime = eventTime;
46
+ completion[cursor] = true;
47
+ cursor += 1;
48
+ continue;
49
+ }
50
+ if (anchorTime && eventTime - anchorTime <= windowMs) {
51
+ completion[cursor] = true;
52
+ cursor += 1;
53
+ }
54
+ }
55
+ return completion;
56
+ }
57
+ }
58
+ function groupByUser(events) {
59
+ const map = new Map;
60
+ for (const event of events) {
61
+ const list = map.get(event.userId) ?? [];
62
+ list.push(event);
63
+ map.set(event.userId, list);
64
+ }
65
+ return map;
66
+ }
67
+ export {
68
+ FunnelAnalyzer
69
+ };
@@ -1,18 +1,14 @@
1
- import { GrowthHypothesis, GrowthMetric } from "../types.js";
2
-
3
- //#region src/growth/hypothesis-generator.d.ts
4
- interface HypothesisGeneratorOptions {
5
- minDelta?: number;
1
+ import type { GrowthHypothesis, GrowthMetric } from '../types';
2
+ export interface HypothesisGeneratorOptions {
3
+ minDelta?: number;
6
4
  }
7
- declare class GrowthHypothesisGenerator {
8
- private readonly minDelta;
9
- constructor(options?: HypothesisGeneratorOptions);
10
- generate(metrics: GrowthMetric[]): GrowthHypothesis[];
11
- private fromMetric;
12
- private delta;
13
- private impact;
14
- private statement;
5
+ export declare class GrowthHypothesisGenerator {
6
+ private readonly minDelta;
7
+ constructor(options?: HypothesisGeneratorOptions);
8
+ generate(metrics: GrowthMetric[]): GrowthHypothesis[];
9
+ private fromMetric;
10
+ private delta;
11
+ private impact;
12
+ private statement;
15
13
  }
16
- //#endregion
17
- export { GrowthHypothesisGenerator, HypothesisGeneratorOptions };
18
14
  //# sourceMappingURL=hypothesis-generator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hypothesis-generator.d.ts","names":[],"sources":["../../src/growth/hypothesis-generator.ts"],"mappings":";;;UAEiB,0BAAA;EACf,QAAA;AAAA;AAAA,cAGW,yBAAA;EAAA,iBACM,QAAA;cAEL,OAAA,GAAU,0BAAA;EAItB,QAAA,CAAS,OAAA,EAAS,YAAA,KAAiB,gBAAA;EAAA,QAQ3B,UAAA;EAAA,QAaA,KAAA;EAAA,QAMA,MAAA;EAAA,QAMA,SAAA;AAAA"}
1
+ {"version":3,"file":"hypothesis-generator.d.ts","sourceRoot":"","sources":["../../src/growth/hypothesis-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE/D,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,OAAO,CAAC,EAAE,0BAA0B;IAIhD,QAAQ,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,gBAAgB,EAAE;IAQrD,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,KAAK;IAMb,OAAO,CAAC,MAAM;IAMd,OAAO,CAAC,SAAS;CAWlB"}