@webority/analytics 0.1.1

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/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@webority/analytics",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "main": "src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js"
8
+ },
9
+ "files": [
10
+ "src"
11
+ ],
12
+ "scripts": {
13
+ "lint": "biome lint ./src",
14
+ "format": "biome format --write ./src",
15
+ "check": "biome check ./src",
16
+ "test": "node test/smoke.mjs"
17
+ },
18
+ "dependencies": {
19
+ "posthog-js": "1.417.1"
20
+ },
21
+ "devDependencies": {
22
+ "@biomejs/biome": "2.5.8"
23
+ }
24
+ }
@@ -0,0 +1,97 @@
1
+ import { ensurePosthogInitialized } from "./posthog-bridge.js";
2
+ import { isActive, isEligible, resetState, state } from "./state.js";
3
+ import { EVENT_NAME_PATTERN } from "./vocabulary.js";
4
+
5
+ export function initAnalytics(config) {
6
+ resetState({
7
+ host: config.host,
8
+ clientKey: config.clientKey,
9
+ product: config.product,
10
+ surface: config.surface,
11
+ environment: config.environment,
12
+ enabled: config.enabled ?? true,
13
+ getConsent: config.getConsent ?? null,
14
+ logger: config.logger ?? null,
15
+ });
16
+
17
+ if (isActive()) {
18
+ ensurePosthogInitialized();
19
+ }
20
+ }
21
+
22
+ export function setConsent(granted) {
23
+ state.consentOverride = granted;
24
+
25
+ if (granted && isEligible()) {
26
+ // A prior revoke leaves an already-initialized instance opted out — re-grant must undo it.
27
+ if (state.posthogInstance) {
28
+ state.posthogInstance.opt_in_capturing();
29
+ } else {
30
+ ensurePosthogInitialized();
31
+ }
32
+ return;
33
+ }
34
+
35
+ if (!granted && state.posthogInstance) {
36
+ state.posthogInstance.opt_out_capturing();
37
+ }
38
+ }
39
+
40
+ export function identify(publicId, props = {}) {
41
+ if (!isActive()) {
42
+ logInactive("identify", { publicId, ...props });
43
+ return;
44
+ }
45
+
46
+ ensurePosthogInitialized().then((posthog) => {
47
+ posthog.identify(publicId, props);
48
+ });
49
+ }
50
+
51
+ export function resetIdentity() {
52
+ if (!state.posthogInstance) {
53
+ return;
54
+ }
55
+ state.posthogInstance.reset();
56
+ }
57
+
58
+ export function registerEvents(names) {
59
+ for (const name of names) {
60
+ if (!EVENT_NAME_PATTERN.test(name)) {
61
+ throw new Error(`@webority/analytics: invalid event name "${name}" — expected snake_case`);
62
+ }
63
+ state.registeredEvents.add(name);
64
+ }
65
+ }
66
+
67
+ export function trackEvent(name, props = {}) {
68
+ if (!state.registeredEvents.has(name)) {
69
+ throw new Error(`@webority/analytics: event "${name}" is not registered — call registerEvents() first`);
70
+ }
71
+ capture(name, props);
72
+ }
73
+
74
+ function capture(name, props) {
75
+ if (!isActive()) {
76
+ logInactive(name, props);
77
+ return;
78
+ }
79
+
80
+ // environment rides on every event: products may share one PostHog project across
81
+ // deployed environments (capnix does), so it is the only reliable env discriminator.
82
+ const merged = {
83
+ ...props,
84
+ product: state.config.product,
85
+ surface: state.config.surface,
86
+ environment: state.config.environment,
87
+ };
88
+ ensurePosthogInitialized().then((posthog) => {
89
+ posthog.capture(name, merged);
90
+ });
91
+ }
92
+
93
+ function logInactive(name, props) {
94
+ if (state.config?.environment === "Local") {
95
+ state.config.logger?.info(name, props);
96
+ }
97
+ }
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { identify, initAnalytics, registerEvents, resetIdentity, setConsent, trackEvent } from "./analytics.js";
2
+ export { track } from "./track-helpers.js";
3
+ export { events } from "./vocabulary.js";
@@ -0,0 +1,28 @@
1
+ import { state } from "./state.js";
2
+
3
+ // Dynamic import keeps posthog-js out of the module graph entirely for callers that
4
+ // never go active (e.g. Local environment) — it assumes browser globals at init time.
5
+ export async function ensurePosthogInitialized() {
6
+ if (state.posthogInstance) {
7
+ return state.posthogInstance;
8
+ }
9
+ if (state.posthogInitPromise) {
10
+ return state.posthogInitPromise;
11
+ }
12
+
13
+ const config = state.config;
14
+ state.posthogInitPromise = import("posthog-js").then((module) => {
15
+ const posthog = module.default ?? module;
16
+ posthog.init(config.clientKey, {
17
+ api_host: config.host,
18
+ autocapture: false,
19
+ capture_pageview: true,
20
+ persistence: "localStorage",
21
+ });
22
+ posthog.register({ product: config.product, surface: config.surface });
23
+ state.posthogInstance = posthog;
24
+ return posthog;
25
+ });
26
+
27
+ return state.posthogInitPromise;
28
+ }
package/src/state.js ADDED
@@ -0,0 +1,40 @@
1
+ import { events } from "./vocabulary.js";
2
+
3
+ export const state = {
4
+ config: null,
5
+ consentOverride: null,
6
+ posthogInstance: null,
7
+ posthogInitPromise: null,
8
+ registeredEvents: new Set(Object.values(events)),
9
+ };
10
+
11
+ export function resetState(config) {
12
+ state.config = config;
13
+ state.consentOverride = null;
14
+ state.posthogInstance = null;
15
+ state.posthogInitPromise = null;
16
+ state.registeredEvents = new Set(Object.values(events));
17
+ }
18
+
19
+ export function isEligible() {
20
+ const config = state.config;
21
+ if (!config) {
22
+ return false;
23
+ }
24
+ return Boolean(config.enabled && config.clientKey && config.environment !== "Local");
25
+ }
26
+
27
+ export function hasConsent() {
28
+ if (state.consentOverride !== null) {
29
+ return state.consentOverride;
30
+ }
31
+ const getConsent = state.config?.getConsent;
32
+ if (typeof getConsent === "function") {
33
+ return Boolean(getConsent());
34
+ }
35
+ return true;
36
+ }
37
+
38
+ export function isActive() {
39
+ return isEligible() && hasConsent();
40
+ }
@@ -0,0 +1,8 @@
1
+ import { trackEvent } from "./analytics.js";
2
+ import { trackHelperNames } from "./vocabulary.js";
3
+
4
+ export const track = Object.freeze(
5
+ Object.fromEntries(
6
+ Object.entries(trackHelperNames).map(([camelName, eventName]) => [camelName, (props = {}) => trackEvent(eventName, props)]),
7
+ ),
8
+ );
@@ -0,0 +1,29 @@
1
+ export const events = Object.freeze({
2
+ SIGNUP_STARTED: "signup_started",
3
+ SIGNUP_COMPLETED: "signup_completed",
4
+ LOGIN_SUCCEEDED: "login_succeeded",
5
+ ONBOARDING_STARTED: "onboarding_started",
6
+ ONBOARDING_STEP_COMPLETED: "onboarding_step_completed",
7
+ ONBOARDING_COMPLETED: "onboarding_completed",
8
+ SUBSCRIPTION_ACTIVATED: "subscription_activated",
9
+ SUBSCRIPTION_CANCELLED: "subscription_cancelled",
10
+ PAYMENT_SUCCEEDED: "payment_succeeded",
11
+ PAYMENT_FAILED: "payment_failed",
12
+ FEEDBACK_SUBMITTED: "feedback_submitted",
13
+ });
14
+
15
+ export const trackHelperNames = Object.freeze({
16
+ signupStarted: events.SIGNUP_STARTED,
17
+ signupCompleted: events.SIGNUP_COMPLETED,
18
+ loginSucceeded: events.LOGIN_SUCCEEDED,
19
+ onboardingStarted: events.ONBOARDING_STARTED,
20
+ onboardingStepCompleted: events.ONBOARDING_STEP_COMPLETED,
21
+ onboardingCompleted: events.ONBOARDING_COMPLETED,
22
+ subscriptionActivated: events.SUBSCRIPTION_ACTIVATED,
23
+ subscriptionCancelled: events.SUBSCRIPTION_CANCELLED,
24
+ paymentSucceeded: events.PAYMENT_SUCCEEDED,
25
+ paymentFailed: events.PAYMENT_FAILED,
26
+ feedbackSubmitted: events.FEEDBACK_SUBMITTED,
27
+ });
28
+
29
+ export const EVENT_NAME_PATTERN = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;