@lssm/lib.analytics 1.42.0 → 1.42.2
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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/churn/index.d.ts +2 -0
- package/dist/churn/index.js +3 -1
- package/dist/churn/predictor.d.ts +22 -0
- package/dist/churn/predictor.d.ts.map +1 -0
- package/dist/churn/predictor.js +74 -1
- package/dist/churn/predictor.js.map +1 -0
- package/dist/cohort/index.d.ts +2 -0
- package/dist/cohort/index.js +3 -1
- package/dist/cohort/tracker.d.ts +9 -0
- package/dist/cohort/tracker.d.ts.map +1 -0
- package/dist/cohort/tracker.js +102 -1
- package/dist/cohort/tracker.js.map +1 -0
- package/dist/funnel/analyzer.d.ts +10 -0
- package/dist/funnel/analyzer.d.ts.map +1 -0
- package/dist/funnel/analyzer.js +64 -1
- package/dist/funnel/analyzer.js.map +1 -0
- package/dist/funnel/index.d.ts +2 -0
- package/dist/funnel/index.js +3 -1
- package/dist/growth/hypothesis-generator.d.ts +18 -0
- package/dist/growth/hypothesis-generator.d.ts.map +1 -0
- package/dist/growth/hypothesis-generator.js +40 -1
- package/dist/growth/hypothesis-generator.js.map +1 -0
- package/dist/growth/index.d.ts +2 -0
- package/dist/growth/index.js +3 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +10 -1
- package/dist/lifecycle/index.d.ts +3 -0
- package/dist/lifecycle/index.js +4 -1
- package/dist/lifecycle/metric-collectors.d.ts +30 -0
- package/dist/lifecycle/metric-collectors.d.ts.map +1 -0
- package/dist/lifecycle/metric-collectors.js +48 -1
- package/dist/lifecycle/metric-collectors.js.map +1 -0
- package/dist/lifecycle/posthog-bridge.d.ts +15 -0
- package/dist/lifecycle/posthog-bridge.d.ts.map +1 -0
- package/dist/lifecycle/posthog-bridge.js +28 -1
- package/dist/lifecycle/posthog-bridge.js.map +1 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +27 -20
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Chaman Ventures, SASU
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
package/dist/churn/index.js
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
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;
|
|
9
|
+
}
|
|
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;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { ChurnPredictor, ChurnPredictorOptions };
|
|
22
|
+
//# sourceMappingURL=predictor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"predictor.d.ts","names":[],"sources":["../../src/churn/predictor.ts"],"sourcesContent":[],"mappings":";;;UAGiB,qBAAA;;EAAA,eAAA,CAAA,EAAA,MAAqB;EAOzB,WAAA,CAAA,EAAA,MAAc;EAMH,SAAA,CAAA,EAAA,MAAA;;AAOW,cAbtB,cAAA,CAasB;EAAW,iBAAA,aAAA;;;;wBAPtB;gBAOR,mBAAmB"}
|
package/dist/churn/predictor.js
CHANGED
|
@@ -1 +1,74 @@
|
|
|
1
|
-
import
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
|
|
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
|
+
};
|
|
58
|
+
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;
|
|
67
|
+
}
|
|
68
|
+
function dateMs(event) {
|
|
69
|
+
return new Date(event.timestamp).getTime();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
export { ChurnPredictor };
|
|
74
|
+
//# sourceMappingURL=predictor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"predictor.js","names":["signals: ChurnSignal[]","drivers: string[]"],"sources":["../../src/churn/predictor.ts"],"sourcesContent":["import dayjs from 'dayjs';\nimport type { AnalyticsEvent, ChurnSignal } from '../types';\n\nexport interface ChurnPredictorOptions {\n recencyWeight?: number;\n frequencyWeight?: number;\n errorWeight?: number;\n decayDays?: number;\n}\n\nexport class ChurnPredictor {\n private readonly recencyWeight: number;\n private readonly frequencyWeight: number;\n private readonly errorWeight: number;\n private readonly decayDays: number;\n\n constructor(options?: ChurnPredictorOptions) {\n this.recencyWeight = options?.recencyWeight ?? 0.5;\n this.frequencyWeight = options?.frequencyWeight ?? 0.3;\n this.errorWeight = options?.errorWeight ?? 0.2;\n this.decayDays = options?.decayDays ?? 14;\n }\n\n score(events: AnalyticsEvent[]): ChurnSignal[] {\n const grouped = groupBy(events, (event) => event.userId);\n const signals: ChurnSignal[] = [];\n for (const [userId, userEvents] of grouped.entries()) {\n const score = this.computeScore(userEvents);\n signals.push({\n userId,\n score,\n bucket: score >= 0.7 ? 'high' : score >= 0.4 ? 'medium' : 'low',\n drivers: this.drivers(userEvents),\n });\n }\n return signals.sort((a, b) => b.score - a.score);\n }\n\n private computeScore(events: AnalyticsEvent[]): number {\n if (!events.length) return 0;\n const sorted = events.sort((a, b) => dateMs(a) - dateMs(b));\n const lastEvent = sorted[sorted.length - 1];\n if (!lastEvent) return 0;\n const daysSinceLast = dayjs().diff(dayjs(lastEvent.timestamp), 'day');\n const recencyScore = Math.max(0, 1 - daysSinceLast / this.decayDays);\n\n const windowStart = dayjs().subtract(this.decayDays, 'day');\n const recentEvents = sorted.filter((event) =>\n dayjs(event.timestamp).isAfter(windowStart)\n );\n const averagePerDay = recentEvents.length / Math.max(this.decayDays, 1);\n const frequencyScore = Math.min(1, averagePerDay * 5);\n\n const errorEvents = recentEvents.filter(\n (event) =>\n typeof event.properties?.error !== 'undefined' ||\n /error|failed/i.test(event.name)\n ).length;\n const errorScore = Math.min(1, errorEvents / 3);\n\n const score =\n recencyScore * this.recencyWeight +\n frequencyScore * this.frequencyWeight +\n (1 - errorScore) * this.errorWeight;\n return Number(score.toFixed(3));\n }\n\n private drivers(events: AnalyticsEvent[]): string[] {\n const drivers: string[] = [];\n const sorted = events.sort((a, b) => dateMs(a) - dateMs(b));\n const lastEvent = sorted[sorted.length - 1];\n if (lastEvent) {\n const days = dayjs().diff(dayjs(lastEvent.timestamp), 'day');\n if (days > this.decayDays) drivers.push(`Inactive for ${days} days`);\n }\n const errorEvents = events.filter(\n (event) =>\n typeof event.properties?.error !== 'undefined' ||\n /error|failed/i.test(event.name)\n );\n if (errorEvents.length) drivers.push(`${errorEvents.length} errors logged`);\n return drivers;\n }\n}\n\nfunction groupBy<T>(\n items: T[],\n selector: (item: T) => string\n): Map<string, T[]> {\n const map = new Map<string, T[]>();\n for (const item of items) {\n const key = selector(item);\n const list = map.get(key) ?? [];\n list.push(item);\n map.set(key, list);\n }\n return map;\n}\n\nfunction dateMs(event: AnalyticsEvent) {\n return new Date(event.timestamp).getTime();\n}\n"],"mappings":";;;AAUA,IAAa,iBAAb,MAA4B;CAC1B,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CAEjB,YAAY,SAAiC;AAC3C,OAAK,gBAAgB,SAAS,iBAAiB;AAC/C,OAAK,kBAAkB,SAAS,mBAAmB;AACnD,OAAK,cAAc,SAAS,eAAe;AAC3C,OAAK,YAAY,SAAS,aAAa;;CAGzC,MAAM,QAAyC;EAC7C,MAAM,UAAU,QAAQ,SAAS,UAAU,MAAM,OAAO;EACxD,MAAMA,UAAyB,EAAE;AACjC,OAAK,MAAM,CAAC,QAAQ,eAAe,QAAQ,SAAS,EAAE;GACpD,MAAM,QAAQ,KAAK,aAAa,WAAW;AAC3C,WAAQ,KAAK;IACX;IACA;IACA,QAAQ,SAAS,KAAM,SAAS,SAAS,KAAM,WAAW;IAC1D,SAAS,KAAK,QAAQ,WAAW;IAClC,CAAC;;AAEJ,SAAO,QAAQ,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM;;CAGlD,AAAQ,aAAa,QAAkC;AACrD,MAAI,CAAC,OAAO,OAAQ,QAAO;EAC3B,MAAM,SAAS,OAAO,MAAM,GAAG,MAAM,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC;EAC3D,MAAM,YAAY,OAAO,OAAO,SAAS;AACzC,MAAI,CAAC,UAAW,QAAO;EACvB,MAAM,gBAAgB,OAAO,CAAC,KAAK,MAAM,UAAU,UAAU,EAAE,MAAM;EACrE,MAAM,eAAe,KAAK,IAAI,GAAG,IAAI,gBAAgB,KAAK,UAAU;EAEpE,MAAM,cAAc,OAAO,CAAC,SAAS,KAAK,WAAW,MAAM;EAC3D,MAAM,eAAe,OAAO,QAAQ,UAClC,MAAM,MAAM,UAAU,CAAC,QAAQ,YAAY,CAC5C;EACD,MAAM,gBAAgB,aAAa,SAAS,KAAK,IAAI,KAAK,WAAW,EAAE;EACvE,MAAM,iBAAiB,KAAK,IAAI,GAAG,gBAAgB,EAAE;EAErD,MAAM,cAAc,aAAa,QAC9B,UACC,OAAO,MAAM,YAAY,UAAU,eACnC,gBAAgB,KAAK,MAAM,KAAK,CACnC,CAAC;EACF,MAAM,aAAa,KAAK,IAAI,GAAG,cAAc,EAAE;EAE/C,MAAM,QACJ,eAAe,KAAK,gBACpB,iBAAiB,KAAK,mBACrB,IAAI,cAAc,KAAK;AAC1B,SAAO,OAAO,MAAM,QAAQ,EAAE,CAAC;;CAGjC,AAAQ,QAAQ,QAAoC;EAClD,MAAMC,UAAoB,EAAE;EAC5B,MAAM,SAAS,OAAO,MAAM,GAAG,MAAM,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC;EAC3D,MAAM,YAAY,OAAO,OAAO,SAAS;AACzC,MAAI,WAAW;GACb,MAAM,OAAO,OAAO,CAAC,KAAK,MAAM,UAAU,UAAU,EAAE,MAAM;AAC5D,OAAI,OAAO,KAAK,UAAW,SAAQ,KAAK,gBAAgB,KAAK,OAAO;;EAEtE,MAAM,cAAc,OAAO,QACxB,UACC,OAAO,MAAM,YAAY,UAAU,eACnC,gBAAgB,KAAK,MAAM,KAAK,CACnC;AACD,MAAI,YAAY,OAAQ,SAAQ,KAAK,GAAG,YAAY,OAAO,gBAAgB;AAC3E,SAAO;;;AAIX,SAAS,QACP,OACA,UACkB;CAClB,MAAM,sBAAM,IAAI,KAAkB;AAClC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,MAAM,SAAS,KAAK;EAC1B,MAAM,OAAO,IAAI,IAAI,IAAI,IAAI,EAAE;AAC/B,OAAK,KAAK,KAAK;AACf,MAAI,IAAI,KAAK,KAAK;;AAEpB,QAAO;;AAGT,SAAS,OAAO,OAAuB;AACrC,QAAO,IAAI,KAAK,MAAM,UAAU,CAAC,SAAS"}
|
package/dist/cohort/index.js
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
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;
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
export { CohortTracker };
|
|
9
|
+
//# sourceMappingURL=tracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracker.d.ts","names":[],"sources":["../../src/cohort/tracker.ts"],"sourcesContent":[],"mappings":";;;cAQa,aAAA;kBAED,8BACI,mBACX;AAJL"}
|
package/dist/cohort/tracker.js
CHANGED
|
@@ -1 +1,102 @@
|
|
|
1
|
-
import
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
|
|
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
|
+
};
|
|
59
|
+
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
|
+
}
|
|
69
|
+
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
|
+
case "month":
|
|
75
|
+
default: return dt.startOf("month").format("YYYY-MM");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function bucketDiff(cohortKey, timestamp, bucket) {
|
|
79
|
+
const start = parseBucketKey(cohortKey, bucket);
|
|
80
|
+
const target = dayjs(timestamp);
|
|
81
|
+
switch (bucket) {
|
|
82
|
+
case "day": return target.diff(start, "day");
|
|
83
|
+
case "week": return target.diff(start, "week");
|
|
84
|
+
case "month":
|
|
85
|
+
default: return target.diff(start, "month");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function parseBucketKey(key, bucket) {
|
|
89
|
+
switch (bucket) {
|
|
90
|
+
case "day": return dayjs(key, "YYYY-MM-DD");
|
|
91
|
+
case "week": return dayjs(key.replace("W", ""), "YYYY-ww");
|
|
92
|
+
case "month":
|
|
93
|
+
default: return dayjs(key, "YYYY-MM");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function dateMs(event) {
|
|
97
|
+
return new Date(event.timestamp).getTime();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
export { CohortTracker };
|
|
102
|
+
//# sourceMappingURL=tracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracker.js","names":["key: string","definition: CohortDefinition","retention: number[]"],"sources":["../../src/cohort/tracker.ts"],"sourcesContent":["import dayjs from 'dayjs';\nimport type {\n AnalyticsEvent,\n CohortAnalysis,\n CohortDefinition,\n CohortStats,\n} from '../types';\n\nexport class CohortTracker {\n analyze(\n events: AnalyticsEvent[],\n definition: CohortDefinition\n ): CohortAnalysis {\n const groupedByUser = groupBy(events, (event) => event.userId);\n const cohorts = new Map<string, CohortStatsBuilder>();\n\n for (const [userId, userEvents] of groupedByUser.entries()) {\n userEvents.sort((a, b) => dateMs(a) - dateMs(b));\n const signup = userEvents[0];\n if (!signup) continue;\n const cohortKey = bucketKey(signup.timestamp, definition.bucket);\n const builder =\n cohorts.get(cohortKey) ?? new CohortStatsBuilder(cohortKey, definition);\n builder.addUser(userId);\n for (const event of userEvents) {\n builder.addEvent(userId, event);\n }\n cohorts.set(cohortKey, builder);\n }\n\n return {\n definition,\n cohorts: [...cohorts.values()].map((builder) => builder.build()),\n };\n }\n}\n\nclass CohortStatsBuilder {\n private readonly users = new Set<string>();\n private readonly retentionMap = new Map<number, Set<string>>();\n private ltv = 0;\n constructor(\n private readonly key: string,\n private readonly definition: CohortDefinition\n ) {}\n\n addUser(userId: string) {\n this.users.add(userId);\n }\n\n addEvent(userId: string, event: AnalyticsEvent) {\n const period = bucketDiff(\n this.key,\n event.timestamp,\n this.definition.bucket\n );\n if (period < 0 || period >= this.definition.periods) return;\n const bucket = this.retentionMap.get(period) ?? new Set<string>();\n bucket.add(userId);\n this.retentionMap.set(period, bucket);\n const amount =\n typeof event.properties?.amount === 'number'\n ? event.properties.amount\n : 0;\n this.ltv += amount;\n }\n\n build(): CohortStats {\n const totalUsers = this.users.size || 1;\n const retention: number[] = [];\n for (let period = 0; period < this.definition.periods; period++) {\n const active = this.retentionMap.get(period)?.size ?? 0;\n retention.push(Number((active / totalUsers).toFixed(3)));\n }\n return {\n cohortKey: this.key,\n users: this.users.size,\n retention,\n ltv: Number(this.ltv.toFixed(2)),\n };\n }\n}\n\nfunction groupBy<T>(\n items: T[],\n selector: (item: T) => string\n): Map<string, T[]> {\n const map = new Map<string, T[]>();\n for (const item of items) {\n const key = selector(item);\n const list = map.get(key) ?? [];\n list.push(item);\n map.set(key, list);\n }\n return map;\n}\n\nfunction bucketKey(\n timestamp: string | Date,\n bucket: CohortDefinition['bucket']\n): string {\n const dt = dayjs(timestamp);\n switch (bucket) {\n case 'day':\n return dt.startOf('day').format('YYYY-MM-DD');\n case 'week':\n return dt.startOf('week').format('YYYY-[W]WW');\n case 'month':\n default:\n return dt.startOf('month').format('YYYY-MM');\n }\n}\n\nfunction bucketDiff(\n cohortKey: string,\n timestamp: string | Date,\n bucket: CohortDefinition['bucket']\n): number {\n const start = parseBucketKey(cohortKey, bucket);\n const target = dayjs(timestamp);\n switch (bucket) {\n case 'day':\n return target.diff(start, 'day');\n case 'week':\n return target.diff(start, 'week');\n case 'month':\n default:\n return target.diff(start, 'month');\n }\n}\n\nfunction parseBucketKey(key: string, bucket: CohortDefinition['bucket']) {\n switch (bucket) {\n case 'day':\n return dayjs(key, 'YYYY-MM-DD');\n case 'week':\n return dayjs(key.replace('W', ''), 'YYYY-ww');\n case 'month':\n default:\n return dayjs(key, 'YYYY-MM');\n }\n}\n\nfunction dateMs(event: AnalyticsEvent) {\n return new Date(event.timestamp).getTime();\n}\n"],"mappings":";;;AAQA,IAAa,gBAAb,MAA2B;CACzB,QACE,QACA,YACgB;EAChB,MAAM,gBAAgB,QAAQ,SAAS,UAAU,MAAM,OAAO;EAC9D,MAAM,0BAAU,IAAI,KAAiC;AAErD,OAAK,MAAM,CAAC,QAAQ,eAAe,cAAc,SAAS,EAAE;AAC1D,cAAW,MAAM,GAAG,MAAM,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC;GAChD,MAAM,SAAS,WAAW;AAC1B,OAAI,CAAC,OAAQ;GACb,MAAM,YAAY,UAAU,OAAO,WAAW,WAAW,OAAO;GAChE,MAAM,UACJ,QAAQ,IAAI,UAAU,IAAI,IAAI,mBAAmB,WAAW,WAAW;AACzE,WAAQ,QAAQ,OAAO;AACvB,QAAK,MAAM,SAAS,WAClB,SAAQ,SAAS,QAAQ,MAAM;AAEjC,WAAQ,IAAI,WAAW,QAAQ;;AAGjC,SAAO;GACL;GACA,SAAS,CAAC,GAAG,QAAQ,QAAQ,CAAC,CAAC,KAAK,YAAY,QAAQ,OAAO,CAAC;GACjE;;;AAIL,IAAM,qBAAN,MAAyB;CACvB,AAAiB,wBAAQ,IAAI,KAAa;CAC1C,AAAiB,+BAAe,IAAI,KAA0B;CAC9D,AAAQ,MAAM;CACd,YACE,AAAiBA,KACjB,AAAiBC,YACjB;EAFiB;EACA;;CAGnB,QAAQ,QAAgB;AACtB,OAAK,MAAM,IAAI,OAAO;;CAGxB,SAAS,QAAgB,OAAuB;EAC9C,MAAM,SAAS,WACb,KAAK,KACL,MAAM,WACN,KAAK,WAAW,OACjB;AACD,MAAI,SAAS,KAAK,UAAU,KAAK,WAAW,QAAS;EACrD,MAAM,SAAS,KAAK,aAAa,IAAI,OAAO,oBAAI,IAAI,KAAa;AACjE,SAAO,IAAI,OAAO;AAClB,OAAK,aAAa,IAAI,QAAQ,OAAO;EACrC,MAAM,SACJ,OAAO,MAAM,YAAY,WAAW,WAChC,MAAM,WAAW,SACjB;AACN,OAAK,OAAO;;CAGd,QAAqB;EACnB,MAAM,aAAa,KAAK,MAAM,QAAQ;EACtC,MAAMC,YAAsB,EAAE;AAC9B,OAAK,IAAI,SAAS,GAAG,SAAS,KAAK,WAAW,SAAS,UAAU;GAC/D,MAAM,SAAS,KAAK,aAAa,IAAI,OAAO,EAAE,QAAQ;AACtD,aAAU,KAAK,QAAQ,SAAS,YAAY,QAAQ,EAAE,CAAC,CAAC;;AAE1D,SAAO;GACL,WAAW,KAAK;GAChB,OAAO,KAAK,MAAM;GAClB;GACA,KAAK,OAAO,KAAK,IAAI,QAAQ,EAAE,CAAC;GACjC;;;AAIL,SAAS,QACP,OACA,UACkB;CAClB,MAAM,sBAAM,IAAI,KAAkB;AAClC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,MAAM,SAAS,KAAK;EAC1B,MAAM,OAAO,IAAI,IAAI,IAAI,IAAI,EAAE;AAC/B,OAAK,KAAK,KAAK;AACf,MAAI,IAAI,KAAK,KAAK;;AAEpB,QAAO;;AAGT,SAAS,UACP,WACA,QACQ;CACR,MAAM,KAAK,MAAM,UAAU;AAC3B,SAAQ,QAAR;EACE,KAAK,MACH,QAAO,GAAG,QAAQ,MAAM,CAAC,OAAO,aAAa;EAC/C,KAAK,OACH,QAAO,GAAG,QAAQ,OAAO,CAAC,OAAO,aAAa;EAChD,KAAK;EACL,QACE,QAAO,GAAG,QAAQ,QAAQ,CAAC,OAAO,UAAU;;;AAIlD,SAAS,WACP,WACA,WACA,QACQ;CACR,MAAM,QAAQ,eAAe,WAAW,OAAO;CAC/C,MAAM,SAAS,MAAM,UAAU;AAC/B,SAAQ,QAAR;EACE,KAAK,MACH,QAAO,OAAO,KAAK,OAAO,MAAM;EAClC,KAAK,OACH,QAAO,OAAO,KAAK,OAAO,OAAO;EACnC,KAAK;EACL,QACE,QAAO,OAAO,KAAK,OAAO,QAAQ;;;AAIxC,SAAS,eAAe,KAAa,QAAoC;AACvE,SAAQ,QAAR;EACE,KAAK,MACH,QAAO,MAAM,KAAK,aAAa;EACjC,KAAK,OACH,QAAO,MAAM,IAAI,QAAQ,KAAK,GAAG,EAAE,UAAU;EAC/C,KAAK;EACL,QACE,QAAO,MAAM,KAAK,UAAU;;;AAIlC,SAAS,OAAO,OAAuB;AACrC,QAAO,IAAI,KAAK,MAAM,UAAU,CAAC,SAAS"}
|
|
@@ -0,0 +1,10 @@
|
|
|
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;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { FunnelAnalyzer };
|
|
10
|
+
//# sourceMappingURL=analyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","names":[],"sources":["../../src/funnel/analyzer.ts"],"sourcesContent":[],"mappings":";;;cAQa,cAAA;kBAED,8BACI,mBACX;EAJQ,QAAA,YAAc"}
|
package/dist/funnel/analyzer.js
CHANGED
|
@@ -1 +1,64 @@
|
|
|
1
|
-
|
|
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
|
+
};
|
|
52
|
+
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;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
export { FunnelAnalyzer };
|
|
64
|
+
//# sourceMappingURL=analyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer.js","names":["completion: boolean[]","anchorTime: number | undefined"],"sources":["../../src/funnel/analyzer.ts"],"sourcesContent":["import type {\n AnalyticsEvent,\n FunnelAnalysis,\n FunnelDefinition,\n FunnelStep,\n FunnelStepResult,\n} from '../types';\n\nexport class FunnelAnalyzer {\n analyze(\n events: AnalyticsEvent[],\n definition: FunnelDefinition\n ): FunnelAnalysis {\n const windowMs = (definition.windowHours ?? 72) * 60 * 60 * 1000;\n const eventsByUser = groupByUser(events);\n const stepCounts = definition.steps.map(() => 0);\n\n for (const userEvents of eventsByUser.values()) {\n const completionIndex = this.evaluateUser(\n userEvents,\n definition.steps,\n windowMs\n );\n completionIndex.forEach((hit, stepIdx) => {\n if (hit) {\n stepCounts[stepIdx] = (stepCounts[stepIdx] ?? 0) + 1;\n }\n });\n }\n\n const totalUsers = eventsByUser.size;\n const steps: FunnelStepResult[] = definition.steps.map((step, index) => {\n const prevCount = index === 0 ? totalUsers : stepCounts[index - 1] || 1;\n const count = stepCounts[index] ?? 0;\n const conversionRate =\n prevCount === 0 ? 0 : Number((count / prevCount).toFixed(3));\n const dropOffRate = Number((1 - conversionRate).toFixed(3));\n return { step, count, conversionRate, dropOffRate };\n });\n\n return {\n definition,\n totalUsers,\n steps,\n };\n }\n\n private evaluateUser(\n events: AnalyticsEvent[],\n steps: FunnelStep[],\n windowMs: number\n ) {\n const sorted = [...events].sort(\n (a, b) =>\n new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()\n );\n const completion: boolean[] = Array(steps.length).fill(false);\n let cursor = 0;\n let anchorTime: number | undefined;\n\n for (const event of sorted) {\n const step = steps[cursor];\n if (!step) break;\n if (event.name !== step.eventName) continue;\n if (step.match && !step.match(event)) continue;\n\n const eventTime = new Date(event.timestamp).getTime();\n if (cursor === 0) {\n anchorTime = eventTime;\n completion[cursor] = true;\n cursor += 1;\n continue;\n }\n\n if (anchorTime && eventTime - anchorTime <= windowMs) {\n completion[cursor] = true;\n cursor += 1;\n }\n }\n\n return completion;\n }\n}\n\nfunction groupByUser(events: AnalyticsEvent[]): Map<string, AnalyticsEvent[]> {\n const map = new Map<string, AnalyticsEvent[]>();\n for (const event of events) {\n const list = map.get(event.userId) ?? [];\n list.push(event);\n map.set(event.userId, list);\n }\n return map;\n}\n"],"mappings":";AAQA,IAAa,iBAAb,MAA4B;CAC1B,QACE,QACA,YACgB;EAChB,MAAM,YAAY,WAAW,eAAe,MAAM,KAAK,KAAK;EAC5D,MAAM,eAAe,YAAY,OAAO;EACxC,MAAM,aAAa,WAAW,MAAM,UAAU,EAAE;AAEhD,OAAK,MAAM,cAAc,aAAa,QAAQ,CAM5C,CALwB,KAAK,aAC3B,YACA,WAAW,OACX,SACD,CACe,SAAS,KAAK,YAAY;AACxC,OAAI,IACF,YAAW,YAAY,WAAW,YAAY,KAAK;IAErD;EAGJ,MAAM,aAAa,aAAa;AAUhC,SAAO;GACL;GACA;GACA,OAZgC,WAAW,MAAM,KAAK,MAAM,UAAU;IACtE,MAAM,YAAY,UAAU,IAAI,aAAa,WAAW,QAAQ,MAAM;IACtE,MAAM,QAAQ,WAAW,UAAU;IACnC,MAAM,iBACJ,cAAc,IAAI,IAAI,QAAQ,QAAQ,WAAW,QAAQ,EAAE,CAAC;AAE9D,WAAO;KAAE;KAAM;KAAO;KAAgB,aADlB,QAAQ,IAAI,gBAAgB,QAAQ,EAAE,CAAC;KACR;KACnD;GAMD;;CAGH,AAAQ,aACN,QACA,OACA,UACA;EACA,MAAM,SAAS,CAAC,GAAG,OAAO,CAAC,MACxB,GAAG,MACF,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,GAAG,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,CACpE;EACD,MAAMA,aAAwB,MAAM,MAAM,OAAO,CAAC,KAAK,MAAM;EAC7D,IAAI,SAAS;EACb,IAAIC;AAEJ,OAAK,MAAM,SAAS,QAAQ;GAC1B,MAAM,OAAO,MAAM;AACnB,OAAI,CAAC,KAAM;AACX,OAAI,MAAM,SAAS,KAAK,UAAW;AACnC,OAAI,KAAK,SAAS,CAAC,KAAK,MAAM,MAAM,CAAE;GAEtC,MAAM,YAAY,IAAI,KAAK,MAAM,UAAU,CAAC,SAAS;AACrD,OAAI,WAAW,GAAG;AAChB,iBAAa;AACb,eAAW,UAAU;AACrB,cAAU;AACV;;AAGF,OAAI,cAAc,YAAY,cAAc,UAAU;AACpD,eAAW,UAAU;AACrB,cAAU;;;AAId,SAAO;;;AAIX,SAAS,YAAY,QAAyD;CAC5E,MAAM,sBAAM,IAAI,KAA+B;AAC/C,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,OAAO,IAAI,IAAI,MAAM,OAAO,IAAI,EAAE;AACxC,OAAK,KAAK,MAAM;AAChB,MAAI,IAAI,MAAM,QAAQ,KAAK;;AAE7B,QAAO"}
|
package/dist/funnel/index.js
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { GrowthHypothesis, GrowthMetric } from "../types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/growth/hypothesis-generator.d.ts
|
|
4
|
+
interface HypothesisGeneratorOptions {
|
|
5
|
+
minDelta?: number;
|
|
6
|
+
}
|
|
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;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { GrowthHypothesisGenerator, HypothesisGeneratorOptions };
|
|
18
|
+
//# sourceMappingURL=hypothesis-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hypothesis-generator.d.ts","names":[],"sources":["../../src/growth/hypothesis-generator.ts"],"sourcesContent":[],"mappings":";;;UAEiB,0BAAA;;AAAjB;AAIa,cAAA,yBAAA,CAAyB;EAGd,iBAAA,QAAA;EAIJ,WAAA,CAAA,OAAA,CAAA,EAJI,0BAIJ;EAAiB,QAAA,CAAA,OAAA,EAAjB,YAAiB,EAAA,CAAA,EAAA,gBAAA,EAAA;EAAgB,QAAA,UAAA"}
|
|
@@ -1 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
//#region src/growth/hypothesis-generator.ts
|
|
2
|
+
var GrowthHypothesisGenerator = class {
|
|
3
|
+
minDelta;
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.minDelta = options?.minDelta ?? .05;
|
|
6
|
+
}
|
|
7
|
+
generate(metrics) {
|
|
8
|
+
return metrics.map((metric) => this.fromMetric(metric)).filter((hypothesis) => Boolean(hypothesis));
|
|
9
|
+
}
|
|
10
|
+
fromMetric(metric) {
|
|
11
|
+
const change = this.delta(metric);
|
|
12
|
+
if (Math.abs(change) < this.minDelta) return null;
|
|
13
|
+
const direction = change > 0 ? "rising" : "declining";
|
|
14
|
+
return {
|
|
15
|
+
statement: this.statement(metric, change, direction),
|
|
16
|
+
metric: metric.name,
|
|
17
|
+
confidence: Math.abs(change) > .2 ? "high" : "medium",
|
|
18
|
+
impact: this.impact(metric)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
delta(metric) {
|
|
22
|
+
if (metric.previous == null) return 0;
|
|
23
|
+
const prev = metric.previous || 1;
|
|
24
|
+
return (metric.current - prev) / Math.abs(prev);
|
|
25
|
+
}
|
|
26
|
+
impact(metric) {
|
|
27
|
+
if (metric.target && metric.current < metric.target * .8) return "high";
|
|
28
|
+
if (metric.target && metric.current < metric.target) return "medium";
|
|
29
|
+
return "low";
|
|
30
|
+
}
|
|
31
|
+
statement(metric, change, direction) {
|
|
32
|
+
const percent = Math.abs(parseFloat((change * 100).toFixed(1)));
|
|
33
|
+
if (direction === "declining") return `${metric.name} is down ${percent}% vs last period; test new onboarding prompts to recover activation.`;
|
|
34
|
+
return `${metric.name} grew ${percent}% period-over-period; double down with expanded experiment or pricing test.`;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { GrowthHypothesisGenerator };
|
|
40
|
+
//# sourceMappingURL=hypothesis-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hypothesis-generator.js","names":[],"sources":["../../src/growth/hypothesis-generator.ts"],"sourcesContent":["import type { GrowthHypothesis, GrowthMetric } from '../types';\n\nexport interface HypothesisGeneratorOptions {\n minDelta?: number;\n}\n\nexport class GrowthHypothesisGenerator {\n private readonly minDelta: number;\n\n constructor(options?: HypothesisGeneratorOptions) {\n this.minDelta = options?.minDelta ?? 0.05;\n }\n\n generate(metrics: GrowthMetric[]): GrowthHypothesis[] {\n return metrics\n .map((metric) => this.fromMetric(metric))\n .filter((hypothesis): hypothesis is GrowthHypothesis =>\n Boolean(hypothesis)\n );\n }\n\n private fromMetric(metric: GrowthMetric): GrowthHypothesis | null {\n const change = this.delta(metric);\n if (Math.abs(change) < this.minDelta) return null;\n const direction = change > 0 ? 'rising' : 'declining';\n const statement = this.statement(metric, change, direction);\n return {\n statement,\n metric: metric.name,\n confidence: Math.abs(change) > 0.2 ? 'high' : 'medium',\n impact: this.impact(metric),\n };\n }\n\n private delta(metric: GrowthMetric): number {\n if (metric.previous == null) return 0;\n const prev = metric.previous || 1;\n return (metric.current - prev) / Math.abs(prev);\n }\n\n private impact(metric: GrowthMetric): GrowthHypothesis['impact'] {\n if (metric.target && metric.current < metric.target * 0.8) return 'high';\n if (metric.target && metric.current < metric.target) return 'medium';\n return 'low';\n }\n\n private statement(\n metric: GrowthMetric,\n change: number,\n direction: string\n ): string {\n const percent = Math.abs(parseFloat((change * 100).toFixed(1)));\n if (direction === 'declining') {\n return `${metric.name} is down ${percent}% vs last period; test new onboarding prompts to recover activation.`;\n }\n return `${metric.name} grew ${percent}% period-over-period; double down with expanded experiment or pricing test.`;\n }\n}\n"],"mappings":";AAMA,IAAa,4BAAb,MAAuC;CACrC,AAAiB;CAEjB,YAAY,SAAsC;AAChD,OAAK,WAAW,SAAS,YAAY;;CAGvC,SAAS,SAA6C;AACpD,SAAO,QACJ,KAAK,WAAW,KAAK,WAAW,OAAO,CAAC,CACxC,QAAQ,eACP,QAAQ,WAAW,CACpB;;CAGL,AAAQ,WAAW,QAA+C;EAChE,MAAM,SAAS,KAAK,MAAM,OAAO;AACjC,MAAI,KAAK,IAAI,OAAO,GAAG,KAAK,SAAU,QAAO;EAC7C,MAAM,YAAY,SAAS,IAAI,WAAW;AAE1C,SAAO;GACL,WAFgB,KAAK,UAAU,QAAQ,QAAQ,UAAU;GAGzD,QAAQ,OAAO;GACf,YAAY,KAAK,IAAI,OAAO,GAAG,KAAM,SAAS;GAC9C,QAAQ,KAAK,OAAO,OAAO;GAC5B;;CAGH,AAAQ,MAAM,QAA8B;AAC1C,MAAI,OAAO,YAAY,KAAM,QAAO;EACpC,MAAM,OAAO,OAAO,YAAY;AAChC,UAAQ,OAAO,UAAU,QAAQ,KAAK,IAAI,KAAK;;CAGjD,AAAQ,OAAO,QAAkD;AAC/D,MAAI,OAAO,UAAU,OAAO,UAAU,OAAO,SAAS,GAAK,QAAO;AAClE,MAAI,OAAO,UAAU,OAAO,UAAU,OAAO,OAAQ,QAAO;AAC5D,SAAO;;CAGT,AAAQ,UACN,QACA,QACA,WACQ;EACR,MAAM,UAAU,KAAK,IAAI,YAAY,SAAS,KAAK,QAAQ,EAAE,CAAC,CAAC;AAC/D,MAAI,cAAc,YAChB,QAAO,GAAG,OAAO,KAAK,WAAW,QAAQ;AAE3C,SAAO,GAAG,OAAO,KAAK,QAAQ,QAAQ"}
|
package/dist/growth/index.js
CHANGED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AnalyticsEvent, ChurnSignal, CohortAnalysis, CohortDefinition, CohortEvent, CohortStats, FunnelAnalysis, FunnelDefinition, FunnelStep, FunnelStepResult, GrowthHypothesis, GrowthMetric } from "./types.js";
|
|
2
|
+
import { ChurnPredictor, ChurnPredictorOptions } from "./churn/predictor.js";
|
|
3
|
+
import { CohortTracker } from "./cohort/tracker.js";
|
|
4
|
+
import { FunnelAnalyzer } from "./funnel/analyzer.js";
|
|
5
|
+
import { GrowthHypothesisGenerator, HypothesisGeneratorOptions } from "./growth/hypothesis-generator.js";
|
|
6
|
+
import { LifecycleMetricSource, LifecycleStageChangePayload, collectLifecycleMetrics, createStageChangeEvent, lifecycleEventNames, metricsToSignals } from "./lifecycle/metric-collectors.js";
|
|
7
|
+
import { PostHogLikeClient, trackLifecycleAssessment, trackLifecycleStageChange } from "./lifecycle/posthog-bridge.js";
|
|
8
|
+
export { AnalyticsEvent, ChurnPredictor, ChurnPredictorOptions, ChurnSignal, CohortAnalysis, CohortDefinition, CohortEvent, CohortStats, CohortTracker, FunnelAnalysis, FunnelAnalyzer, FunnelDefinition, FunnelStep, FunnelStepResult, GrowthHypothesis, GrowthHypothesisGenerator, GrowthMetric, HypothesisGeneratorOptions, LifecycleMetricSource, LifecycleStageChangePayload, PostHogLikeClient, collectLifecycleMetrics, createStageChangeEvent, lifecycleEventNames, metricsToSignals, trackLifecycleAssessment, trackLifecycleStageChange };
|
package/dist/index.js
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { FunnelAnalyzer } from "./funnel/analyzer.js";
|
|
2
|
+
import { CohortTracker } from "./cohort/tracker.js";
|
|
3
|
+
import "./cohort/index.js";
|
|
4
|
+
import { ChurnPredictor } from "./churn/predictor.js";
|
|
5
|
+
import "./churn/index.js";
|
|
6
|
+
import { GrowthHypothesisGenerator } from "./growth/hypothesis-generator.js";
|
|
7
|
+
import { collectLifecycleMetrics, createStageChangeEvent, lifecycleEventNames, metricsToSignals } from "./lifecycle/metric-collectors.js";
|
|
8
|
+
import { trackLifecycleAssessment, trackLifecycleStageChange } from "./lifecycle/posthog-bridge.js";
|
|
9
|
+
|
|
10
|
+
export { ChurnPredictor, CohortTracker, FunnelAnalyzer, GrowthHypothesisGenerator, collectLifecycleMetrics, createStageChangeEvent, lifecycleEventNames, metricsToSignals, trackLifecycleAssessment, trackLifecycleStageChange };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { LifecycleMetricSource, LifecycleStageChangePayload, collectLifecycleMetrics, createStageChangeEvent, lifecycleEventNames, metricsToSignals } from "./metric-collectors.js";
|
|
2
|
+
import { PostHogLikeClient, trackLifecycleAssessment, trackLifecycleStageChange } from "./posthog-bridge.js";
|
|
3
|
+
export { LifecycleMetricSource, LifecycleStageChangePayload, PostHogLikeClient, collectLifecycleMetrics, createStageChangeEvent, lifecycleEventNames, metricsToSignals, trackLifecycleAssessment, trackLifecycleStageChange };
|
package/dist/lifecycle/index.js
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
|
-
import{collectLifecycleMetrics
|
|
1
|
+
import { collectLifecycleMetrics, createStageChangeEvent, lifecycleEventNames, metricsToSignals } from "./metric-collectors.js";
|
|
2
|
+
import { trackLifecycleAssessment, trackLifecycleStageChange } from "./posthog-bridge.js";
|
|
3
|
+
|
|
4
|
+
export { collectLifecycleMetrics, createStageChangeEvent, lifecycleEventNames, metricsToSignals, trackLifecycleAssessment, trackLifecycleStageChange };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AnalyticsEvent } from "../types.js";
|
|
2
|
+
import { LifecycleMetricSnapshot, LifecycleSignal, LifecycleStage } from "@lssm/lib.lifecycle";
|
|
3
|
+
|
|
4
|
+
//#region src/lifecycle/metric-collectors.d.ts
|
|
5
|
+
interface LifecycleMetricSource {
|
|
6
|
+
getActiveUsers(): Promise<number | undefined>;
|
|
7
|
+
getWeeklyActiveUsers?(): Promise<number | undefined>;
|
|
8
|
+
getRetentionRate?(): Promise<number | undefined>;
|
|
9
|
+
getMonthlyRecurringRevenue?(): Promise<number | undefined>;
|
|
10
|
+
getCustomerCount?(): Promise<number | undefined>;
|
|
11
|
+
getTeamSize?(): Promise<number | undefined>;
|
|
12
|
+
getBurnMultiple?(): Promise<number | undefined>;
|
|
13
|
+
}
|
|
14
|
+
declare const collectLifecycleMetrics: (source: LifecycleMetricSource) => Promise<LifecycleMetricSnapshot>;
|
|
15
|
+
declare const metricsToSignals: (metrics: LifecycleMetricSnapshot, tenantId?: string) => LifecycleSignal[];
|
|
16
|
+
declare const lifecycleEventNames: {
|
|
17
|
+
readonly assessmentRun: "lifecycle_assessment_run";
|
|
18
|
+
readonly stageChanged: "lifecycle_stage_changed";
|
|
19
|
+
readonly guidanceConsumed: "lifecycle_guidance_consumed";
|
|
20
|
+
};
|
|
21
|
+
interface LifecycleStageChangePayload {
|
|
22
|
+
tenantId?: string;
|
|
23
|
+
previousStage?: LifecycleStage;
|
|
24
|
+
nextStage: LifecycleStage;
|
|
25
|
+
confidence: number;
|
|
26
|
+
}
|
|
27
|
+
declare const createStageChangeEvent: (payload: LifecycleStageChangePayload) => AnalyticsEvent;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { LifecycleMetricSource, LifecycleStageChangePayload, collectLifecycleMetrics, createStageChangeEvent, lifecycleEventNames, metricsToSignals };
|
|
30
|
+
//# sourceMappingURL=metric-collectors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metric-collectors.d.ts","names":[],"sources":["../../src/lifecycle/metric-collectors.ts"],"sourcesContent":[],"mappings":";;;;UAOiB,qBAAA;oBACG;EADH,oBAAA,GAAA,EAEU,OAFW,CAAA,MAAA,GAAA,SAAA,CAAA;EAClB,gBAAA,GAAA,EAEG,OAFH,CAAA,MAAA,GAAA,SAAA,CAAA;EACO,0BAAA,GAAA,EAEM,OAFN,CAAA,MAAA,GAAA,SAAA,CAAA;EACJ,gBAAA,GAAA,EAEA,OAFA,CAAA,MAAA,GAAA,SAAA,CAAA;EACU,WAAA,GAAA,EAEf,OAFe,CAAA,MAAA,GAAA,SAAA,CAAA;EACV,eAAA,GAAA,EAED,OAFC,CAAA,MAAA,GAAA,SAAA,CAAA;;AAED,cAGT,uBAHS,EAAA,CAAA,MAAA,EAIZ,qBAJY,EAAA,GAKnB,OALmB,CAKX,uBALW,CAAA;AAAO,cAmChB,gBAnCgB,EAAA,CAAA,OAAA,EAoClB,uBApCkB,EAAA,QAAA,CAAA,EAAA,MAAA,EAAA,GAsC1B,eAtC0B,EAAA;AAGhB,cAkDA,mBApBZ,EAAA;EA7BS,SAAA,aAAA,EAAA,0BAAA;EACC,SAAA,YAAA,EAAA,yBAAA;EAAR,SAAA,gBAAA,EAAA,6BAAA;CAAO;AA8BG,UAwBI,2BAAA,CAvBN;EAiBE,QAAA,CAAA,EAAA,MAAA;EAMI,aAAA,CAAA,EAEC,cAF0B;EAO/B,SAAA,EAJA,cAYX;;;cARW,kCACF,gCACR"}
|
|
@@ -1 +1,48 @@
|
|
|
1
|
-
|
|
1
|
+
//#region src/lifecycle/metric-collectors.ts
|
|
2
|
+
const collectLifecycleMetrics = async (source) => {
|
|
3
|
+
const [activeUsers, weeklyActiveUsers, retentionRate, monthlyRecurringRevenue, customerCount, teamSize, burnMultiple] = await Promise.all([
|
|
4
|
+
source.getActiveUsers(),
|
|
5
|
+
source.getWeeklyActiveUsers?.(),
|
|
6
|
+
source.getRetentionRate?.(),
|
|
7
|
+
source.getMonthlyRecurringRevenue?.(),
|
|
8
|
+
source.getCustomerCount?.(),
|
|
9
|
+
source.getTeamSize?.(),
|
|
10
|
+
source.getBurnMultiple?.()
|
|
11
|
+
]);
|
|
12
|
+
return {
|
|
13
|
+
activeUsers,
|
|
14
|
+
weeklyActiveUsers,
|
|
15
|
+
retentionRate,
|
|
16
|
+
monthlyRecurringRevenue,
|
|
17
|
+
customerCount,
|
|
18
|
+
teamSize,
|
|
19
|
+
burnMultiple
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
const metricsToSignals = (metrics, tenantId) => Object.entries(metrics).filter(([, value]) => value !== void 0 && value !== null).map(([metricKey, value]) => ({
|
|
23
|
+
id: `lifecycle-metric:${metricKey}`,
|
|
24
|
+
kind: "metric",
|
|
25
|
+
source: "analytics",
|
|
26
|
+
name: metricKey,
|
|
27
|
+
value,
|
|
28
|
+
weight: 1,
|
|
29
|
+
confidence: .8,
|
|
30
|
+
details: tenantId ? { tenantId } : void 0,
|
|
31
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
32
|
+
}));
|
|
33
|
+
const lifecycleEventNames = {
|
|
34
|
+
assessmentRun: "lifecycle_assessment_run",
|
|
35
|
+
stageChanged: "lifecycle_stage_changed",
|
|
36
|
+
guidanceConsumed: "lifecycle_guidance_consumed"
|
|
37
|
+
};
|
|
38
|
+
const createStageChangeEvent = (payload) => ({
|
|
39
|
+
name: lifecycleEventNames.stageChanged,
|
|
40
|
+
userId: "system",
|
|
41
|
+
tenantId: payload.tenantId,
|
|
42
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
43
|
+
properties: { ...payload }
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { collectLifecycleMetrics, createStageChangeEvent, lifecycleEventNames, metricsToSignals };
|
|
48
|
+
//# sourceMappingURL=metric-collectors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metric-collectors.js","names":[],"sources":["../../src/lifecycle/metric-collectors.ts"],"sourcesContent":["import type {\n LifecycleMetricSnapshot,\n LifecycleSignal,\n LifecycleStage,\n} from '@lssm/lib.lifecycle';\nimport type { AnalyticsEvent } from '../types';\n\nexport interface LifecycleMetricSource {\n getActiveUsers(): Promise<number | undefined>;\n getWeeklyActiveUsers?(): Promise<number | undefined>;\n getRetentionRate?(): Promise<number | undefined>;\n getMonthlyRecurringRevenue?(): Promise<number | undefined>;\n getCustomerCount?(): Promise<number | undefined>;\n getTeamSize?(): Promise<number | undefined>;\n getBurnMultiple?(): Promise<number | undefined>;\n}\n\nexport const collectLifecycleMetrics = async (\n source: LifecycleMetricSource\n): Promise<LifecycleMetricSnapshot> => {\n const [\n activeUsers,\n weeklyActiveUsers,\n retentionRate,\n monthlyRecurringRevenue,\n customerCount,\n teamSize,\n burnMultiple,\n ] = await Promise.all([\n source.getActiveUsers(),\n source.getWeeklyActiveUsers?.(),\n source.getRetentionRate?.(),\n source.getMonthlyRecurringRevenue?.(),\n source.getCustomerCount?.(),\n source.getTeamSize?.(),\n source.getBurnMultiple?.(),\n ]);\n\n return {\n activeUsers,\n weeklyActiveUsers,\n retentionRate,\n monthlyRecurringRevenue,\n customerCount,\n teamSize,\n burnMultiple,\n };\n};\n\nexport const metricsToSignals = (\n metrics: LifecycleMetricSnapshot,\n tenantId?: string\n): LifecycleSignal[] =>\n Object.entries(metrics)\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([metricKey, value]) => ({\n id: `lifecycle-metric:${metricKey}`,\n kind: 'metric',\n source: 'analytics',\n name: metricKey,\n value,\n weight: 1,\n confidence: 0.8,\n details: tenantId ? { tenantId } : undefined,\n capturedAt: new Date().toISOString(),\n }));\n\nexport const lifecycleEventNames = {\n assessmentRun: 'lifecycle_assessment_run',\n stageChanged: 'lifecycle_stage_changed',\n guidanceConsumed: 'lifecycle_guidance_consumed',\n} as const;\n\nexport interface LifecycleStageChangePayload {\n tenantId?: string;\n previousStage?: LifecycleStage;\n nextStage: LifecycleStage;\n confidence: number;\n}\n\nexport const createStageChangeEvent = (\n payload: LifecycleStageChangePayload\n): AnalyticsEvent => ({\n name: lifecycleEventNames.stageChanged,\n userId: 'system',\n tenantId: payload.tenantId,\n timestamp: new Date(),\n properties: { ...payload },\n});\n"],"mappings":";AAiBA,MAAa,0BAA0B,OACrC,WACqC;CACrC,MAAM,CACJ,aACA,mBACA,eACA,yBACA,eACA,UACA,gBACE,MAAM,QAAQ,IAAI;EACpB,OAAO,gBAAgB;EACvB,OAAO,wBAAwB;EAC/B,OAAO,oBAAoB;EAC3B,OAAO,8BAA8B;EACrC,OAAO,oBAAoB;EAC3B,OAAO,eAAe;EACtB,OAAO,mBAAmB;EAC3B,CAAC;AAEF,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACD;;AAGH,MAAa,oBACX,SACA,aAEA,OAAO,QAAQ,QAAQ,CACpB,QAAQ,GAAG,WAAW,UAAU,UAAa,UAAU,KAAK,CAC5D,KAAK,CAAC,WAAW,YAAY;CAC5B,IAAI,oBAAoB;CACxB,MAAM;CACN,QAAQ;CACR,MAAM;CACN;CACA,QAAQ;CACR,YAAY;CACZ,SAAS,WAAW,EAAE,UAAU,GAAG;CACnC,6BAAY,IAAI,MAAM,EAAC,aAAa;CACrC,EAAE;AAEP,MAAa,sBAAsB;CACjC,eAAe;CACf,cAAc;CACd,kBAAkB;CACnB;AASD,MAAa,0BACX,aACoB;CACpB,MAAM,oBAAoB;CAC1B,QAAQ;CACR,UAAU,QAAQ;CAClB,2BAAW,IAAI,MAAM;CACrB,YAAY,EAAE,GAAG,SAAS;CAC3B"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LifecycleAssessment } from "@lssm/lib.lifecycle";
|
|
2
|
+
|
|
3
|
+
//#region src/lifecycle/posthog-bridge.d.ts
|
|
4
|
+
interface PostHogLikeClient {
|
|
5
|
+
capture: (event: {
|
|
6
|
+
distinctId: string;
|
|
7
|
+
event: string;
|
|
8
|
+
properties?: Record<string, unknown>;
|
|
9
|
+
}) => Promise<void> | void;
|
|
10
|
+
}
|
|
11
|
+
declare const trackLifecycleAssessment: (client: PostHogLikeClient, tenantId: string, assessment: LifecycleAssessment) => Promise<void>;
|
|
12
|
+
declare const trackLifecycleStageChange: (client: PostHogLikeClient, tenantId: string, previousStage: number | undefined, nextStage: number) => Promise<void>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { PostHogLikeClient, trackLifecycleAssessment, trackLifecycleStageChange };
|
|
15
|
+
//# sourceMappingURL=posthog-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"posthog-bridge.d.ts","names":[],"sources":["../../src/lifecycle/posthog-bridge.ts"],"sourcesContent":[],"mappings":";;;UAGiB,iBAAA;;IAAA,UAAA,EAAA,MAAiB;IAQrB,KAAA,EAAA,MAAA;IACH,UAAA,CAAA,EALO,MAKP,CAAA,MAAA,EAAA,OAAA,CAAA;EAEI,CAAA,EAAA,GANN,OAMM,CAAA,IAAA,CAAA,GAAA,IAAA;;AAAmB,cAHpB,wBAGoB,EAAA,CAAA,MAAA,EAFvB,iBAEuB,EAAA,QAAA,EAAA,MAAA,EAAA,UAAA,EAAnB,mBAAmB,EAAA,GAAA,OAAA,CAAA,IAAA,CAAA;AAapB,cAAA,yBACH,EAAA,CAAA,MAAA,EAAA,iBAGS,EAAA,QAAA,EAAA,MAAA,EAAA,aAAA,EAAA,MAAA,GAAA,SAAA,EAAA,SAAA,EAAA,MAAA,EAAA,GAAA,OAAA,CAAA,IAAA,CAAA"}
|
|
@@ -1 +1,28 @@
|
|
|
1
|
-
import{lifecycleEventNames
|
|
1
|
+
import { lifecycleEventNames } from "./metric-collectors.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lifecycle/posthog-bridge.ts
|
|
4
|
+
const trackLifecycleAssessment = async (client, tenantId, assessment) => {
|
|
5
|
+
await client.capture({
|
|
6
|
+
distinctId: tenantId,
|
|
7
|
+
event: lifecycleEventNames.assessmentRun,
|
|
8
|
+
properties: {
|
|
9
|
+
stage: assessment.stage,
|
|
10
|
+
confidence: assessment.confidence,
|
|
11
|
+
axes: assessment.axes
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
const trackLifecycleStageChange = async (client, tenantId, previousStage, nextStage) => {
|
|
16
|
+
await client.capture({
|
|
17
|
+
distinctId: tenantId,
|
|
18
|
+
event: lifecycleEventNames.stageChanged,
|
|
19
|
+
properties: {
|
|
20
|
+
previousStage,
|
|
21
|
+
nextStage
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { trackLifecycleAssessment, trackLifecycleStageChange };
|
|
28
|
+
//# sourceMappingURL=posthog-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"posthog-bridge.js","names":[],"sources":["../../src/lifecycle/posthog-bridge.ts"],"sourcesContent":["import type { LifecycleAssessment } from '@lssm/lib.lifecycle';\nimport { lifecycleEventNames } from './metric-collectors';\n\nexport interface PostHogLikeClient {\n capture: (event: {\n distinctId: string;\n event: string;\n properties?: Record<string, unknown>;\n }) => Promise<void> | void;\n}\n\nexport const trackLifecycleAssessment = async (\n client: PostHogLikeClient,\n tenantId: string,\n assessment: LifecycleAssessment\n) => {\n await client.capture({\n distinctId: tenantId,\n event: lifecycleEventNames.assessmentRun,\n properties: {\n stage: assessment.stage,\n confidence: assessment.confidence,\n axes: assessment.axes,\n },\n });\n};\n\nexport const trackLifecycleStageChange = async (\n client: PostHogLikeClient,\n tenantId: string,\n previousStage: number | undefined,\n nextStage: number\n) => {\n await client.capture({\n distinctId: tenantId,\n event: lifecycleEventNames.stageChanged,\n properties: {\n previousStage,\n nextStage,\n },\n });\n};\n"],"mappings":";;;AAWA,MAAa,2BAA2B,OACtC,QACA,UACA,eACG;AACH,OAAM,OAAO,QAAQ;EACnB,YAAY;EACZ,OAAO,oBAAoB;EAC3B,YAAY;GACV,OAAO,WAAW;GAClB,YAAY,WAAW;GACvB,MAAM,WAAW;GAClB;EACF,CAAC;;AAGJ,MAAa,4BAA4B,OACvC,QACA,UACA,eACA,cACG;AACH,OAAM,OAAO,QAAQ;EACnB,YAAY;EACZ,OAAO,oBAAoB;EAC3B,YAAY;GACV;GACA;GACD;EACF,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
interface AnalyticsEvent {
|
|
3
|
+
name: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
tenantId?: string;
|
|
6
|
+
timestamp: string | Date;
|
|
7
|
+
properties?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
interface FunnelStep {
|
|
10
|
+
id: string;
|
|
11
|
+
eventName: string;
|
|
12
|
+
match?: (event: AnalyticsEvent) => boolean;
|
|
13
|
+
}
|
|
14
|
+
interface FunnelDefinition {
|
|
15
|
+
name: string;
|
|
16
|
+
steps: FunnelStep[];
|
|
17
|
+
windowHours?: number;
|
|
18
|
+
}
|
|
19
|
+
interface FunnelStepResult {
|
|
20
|
+
step: FunnelStep;
|
|
21
|
+
count: number;
|
|
22
|
+
conversionRate: number;
|
|
23
|
+
dropOffRate: number;
|
|
24
|
+
}
|
|
25
|
+
interface FunnelAnalysis {
|
|
26
|
+
definition: FunnelDefinition;
|
|
27
|
+
totalUsers: number;
|
|
28
|
+
steps: FunnelStepResult[];
|
|
29
|
+
}
|
|
30
|
+
interface CohortEvent extends AnalyticsEvent {
|
|
31
|
+
amount?: number;
|
|
32
|
+
}
|
|
33
|
+
interface CohortDefinition {
|
|
34
|
+
bucket: 'day' | 'week' | 'month';
|
|
35
|
+
periods: number;
|
|
36
|
+
startDate?: Date;
|
|
37
|
+
}
|
|
38
|
+
interface CohortStats {
|
|
39
|
+
cohortKey: string;
|
|
40
|
+
users: number;
|
|
41
|
+
retention: number[];
|
|
42
|
+
ltv: number;
|
|
43
|
+
}
|
|
44
|
+
interface CohortAnalysis {
|
|
45
|
+
definition: CohortDefinition;
|
|
46
|
+
cohorts: CohortStats[];
|
|
47
|
+
}
|
|
48
|
+
interface ChurnSignal {
|
|
49
|
+
userId: string;
|
|
50
|
+
score: number;
|
|
51
|
+
bucket: 'low' | 'medium' | 'high';
|
|
52
|
+
drivers: string[];
|
|
53
|
+
}
|
|
54
|
+
interface GrowthMetric {
|
|
55
|
+
name: string;
|
|
56
|
+
current: number;
|
|
57
|
+
previous?: number;
|
|
58
|
+
target?: number;
|
|
59
|
+
}
|
|
60
|
+
interface GrowthHypothesis {
|
|
61
|
+
statement: string;
|
|
62
|
+
metric: string;
|
|
63
|
+
confidence: 'low' | 'medium' | 'high';
|
|
64
|
+
impact: 'low' | 'medium' | 'high';
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { AnalyticsEvent, ChurnSignal, CohortAnalysis, CohortDefinition, CohortEvent, CohortStats, FunnelAnalysis, FunnelDefinition, FunnelStep, FunnelStepResult, GrowthHypothesis, GrowthMetric };
|
|
68
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";UAAiB,cAAA;EAAA,IAAA,EAAA,MAAA;EAQA,MAAA,EAAA,MAAU;EAMV,QAAA,CAAA,EAAA,MAAA;EAMA,SAAA,EAAA,MAAA,GAhBK,IAgBW;EAOhB,UAAA,CAAA,EAtBF,MAsBgB,CAAA,MAAA,EACjB,OAAA,CAAA;AAKd;AAIiB,UA7BA,UAAA,CA6BgB;EAMhB,EAAA,EAAA,MAAA;EAOA,SAAA,EAAA,MAAc;EAKd,KAAA,CAAA,EAAA,CAAA,KAAW,EA5CV,cA4CU,EAAA,GAAA,OAAA;AAO5B;AAOiB,UAvDA,gBAAA,CAuDgB;;SArDxB;;;UAIQ,gBAAA;QACT;;;;;UAMS,cAAA;cACH;;SAEL;;UAGQ,WAAA,SAAoB;;;UAIpB,gBAAA;;;cAGH;;UAGG,WAAA;;;;;;UAOA,cAAA;cACH;WACH;;UAGM,WAAA;;;;;;UAOA,YAAA;;;;;;UAOA,gBAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lssm/lib.analytics",
|
|
3
|
-
"version": "1.42.
|
|
3
|
+
"version": "1.42.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
|
|
14
14
|
"publish:pkg:canary": "bun publish:pkg --tag canary",
|
|
15
|
-
"build": "bun build:
|
|
15
|
+
"build": "bun build:types && bun build:bundle",
|
|
16
16
|
"build:bundle": "tsdown",
|
|
17
17
|
"build:types": "tsc --noEmit",
|
|
18
18
|
"dev": "bun build:bundle --watch",
|
|
@@ -23,29 +23,29 @@
|
|
|
23
23
|
"test": "bun run"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@lssm/lib.lifecycle": "1.42.
|
|
26
|
+
"@lssm/lib.lifecycle": "1.42.2",
|
|
27
27
|
"dayjs": "^1.11.13"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@lssm/tool.tsdown": "1.42.
|
|
31
|
-
"@lssm/tool.typescript": "1.42.
|
|
32
|
-
"tsdown": "^0.
|
|
30
|
+
"@lssm/tool.tsdown": "1.42.2",
|
|
31
|
+
"@lssm/tool.typescript": "1.42.2",
|
|
32
|
+
"tsdown": "^0.18.3",
|
|
33
33
|
"typescript": "^5.9.3"
|
|
34
34
|
},
|
|
35
35
|
"exports": {
|
|
36
|
-
".": "./
|
|
37
|
-
"./churn": "./
|
|
38
|
-
"./churn/predictor": "./
|
|
39
|
-
"./cohort": "./
|
|
40
|
-
"./cohort/tracker": "./
|
|
41
|
-
"./funnel": "./
|
|
42
|
-
"./funnel/analyzer": "./
|
|
43
|
-
"./growth": "./
|
|
44
|
-
"./growth/hypothesis-generator": "./
|
|
45
|
-
"./lifecycle": "./
|
|
46
|
-
"./lifecycle/metric-collectors": "./
|
|
47
|
-
"./lifecycle/posthog-bridge": "./
|
|
48
|
-
"./types": "./
|
|
36
|
+
".": "./dist/index.js",
|
|
37
|
+
"./churn": "./dist/churn/index.js",
|
|
38
|
+
"./churn/predictor": "./dist/churn/predictor.js",
|
|
39
|
+
"./cohort": "./dist/cohort/index.js",
|
|
40
|
+
"./cohort/tracker": "./dist/cohort/tracker.js",
|
|
41
|
+
"./funnel": "./dist/funnel/index.js",
|
|
42
|
+
"./funnel/analyzer": "./dist/funnel/analyzer.js",
|
|
43
|
+
"./growth": "./dist/growth/index.js",
|
|
44
|
+
"./growth/hypothesis-generator": "./dist/growth/hypothesis-generator.js",
|
|
45
|
+
"./lifecycle": "./dist/lifecycle/index.js",
|
|
46
|
+
"./lifecycle/metric-collectors": "./dist/lifecycle/metric-collectors.js",
|
|
47
|
+
"./lifecycle/posthog-bridge": "./dist/lifecycle/posthog-bridge.js",
|
|
48
|
+
"./types": "./dist/types.js",
|
|
49
49
|
"./*": "./*"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
@@ -65,6 +65,13 @@
|
|
|
65
65
|
"./lifecycle/posthog-bridge": "./dist/lifecycle/posthog-bridge.js",
|
|
66
66
|
"./types": "./dist/types.js",
|
|
67
67
|
"./*": "./*"
|
|
68
|
-
}
|
|
68
|
+
},
|
|
69
|
+
"registry": "https://registry.npmjs.org/"
|
|
70
|
+
},
|
|
71
|
+
"license": "MIT",
|
|
72
|
+
"repository": {
|
|
73
|
+
"type": "git",
|
|
74
|
+
"url": "https://github.com/lssm-tech/contractspec.git",
|
|
75
|
+
"directory": "packages/libs/analytics"
|
|
69
76
|
}
|
|
70
77
|
}
|