@codaco/analytics 8.0.0 → 9.0.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/CHANGELOG.md +36 -0
- package/MIGRATION.md +404 -0
- package/README.md +486 -4
- package/dist/chunk-3NEQVIC4.js +72 -0
- package/dist/chunk-3NEQVIC4.js.map +1 -0
- package/dist/index.d.ts +113 -82
- package/dist/index.js +188 -160
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +44 -0
- package/dist/server.js +153 -0
- package/dist/server.js.map +1 -0
- package/dist/types-DK5BiTnW.d.ts +145 -0
- package/package.json +27 -7
- package/src/__tests__/client.test.ts +276 -0
- package/src/__tests__/index.test.ts +207 -0
- package/src/__tests__/utils.test.ts +105 -0
- package/src/client.ts +151 -0
- package/src/config.ts +92 -0
- package/src/hooks.ts +79 -0
- package/src/index.ts +69 -237
- package/src/provider.tsx +60 -0
- package/src/server.ts +213 -0
- package/src/types.ts +183 -0
- package/src/utils.ts +1 -0
- package/tsconfig.json +2 -2
- package/vitest.config.ts +18 -0
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { A as AnalyticsConfig, a as Analytics } from './types-DK5BiTnW.js';
|
|
2
|
+
import 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Initialize server-side analytics
|
|
6
|
+
* Call this once in your app (e.g., in a layout or middleware)
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* // In your Next.js layout or API route
|
|
11
|
+
* import { initServerAnalytics } from '@codaco/analytics/server';
|
|
12
|
+
*
|
|
13
|
+
* initServerAnalytics({
|
|
14
|
+
* installationId: 'your-unique-installation-id',
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
declare function initServerAnalytics(config: AnalyticsConfig): void;
|
|
19
|
+
/**
|
|
20
|
+
* Get the server-side analytics instance
|
|
21
|
+
* Use this in server components, API routes, and server actions
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { getServerAnalytics } from '@codaco/analytics/server';
|
|
26
|
+
*
|
|
27
|
+
* export async function POST(request: Request) {
|
|
28
|
+
* const analytics = getServerAnalytics();
|
|
29
|
+
* analytics.trackEvent('data_exported', {
|
|
30
|
+
* metadata: { format: 'csv' }
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* // ... rest of your handler
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
declare function getServerAnalytics(): Analytics;
|
|
38
|
+
/**
|
|
39
|
+
* Convenience export for direct usage
|
|
40
|
+
* Requires calling initServerAnalytics() first
|
|
41
|
+
*/
|
|
42
|
+
declare const serverAnalytics: Analytics;
|
|
43
|
+
|
|
44
|
+
export { getServerAnalytics, initServerAnalytics, serverAnalytics };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureError,
|
|
3
|
+
mergeConfig
|
|
4
|
+
} from "./chunk-3NEQVIC4.js";
|
|
5
|
+
|
|
6
|
+
// src/server.ts
|
|
7
|
+
var ServerAnalytics = class {
|
|
8
|
+
config;
|
|
9
|
+
disabled;
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.config = mergeConfig(config);
|
|
12
|
+
this.disabled = this.config.disabled;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Track an event on the server-side
|
|
16
|
+
*/
|
|
17
|
+
trackEvent(eventType, properties) {
|
|
18
|
+
if (this.disabled) return;
|
|
19
|
+
this.sendToPostHog(eventType, {
|
|
20
|
+
...properties,
|
|
21
|
+
...properties?.metadata ?? {}
|
|
22
|
+
}).catch((_error) => {
|
|
23
|
+
if (this.config.debug) {
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Track an error on the server-side
|
|
29
|
+
*/
|
|
30
|
+
trackError(error, additionalProperties) {
|
|
31
|
+
if (this.disabled) return;
|
|
32
|
+
const errorObj = ensureError(error);
|
|
33
|
+
const errorProperties = {
|
|
34
|
+
message: errorObj.message,
|
|
35
|
+
name: errorObj.name,
|
|
36
|
+
stack: errorObj.stack,
|
|
37
|
+
cause: errorObj.cause ? String(errorObj.cause) : void 0,
|
|
38
|
+
...additionalProperties
|
|
39
|
+
};
|
|
40
|
+
this.sendToPostHog("error", {
|
|
41
|
+
...errorProperties,
|
|
42
|
+
...additionalProperties?.metadata ?? {}
|
|
43
|
+
}).catch((_error) => {
|
|
44
|
+
if (this.config.debug) {
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Feature flags are not supported in server-side mode
|
|
50
|
+
* Use client-side hooks or PostHog API directly for feature flags
|
|
51
|
+
*/
|
|
52
|
+
isFeatureEnabled(_flagKey) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Feature flags are not supported in server-side mode
|
|
57
|
+
*/
|
|
58
|
+
getFeatureFlag(_flagKey) {
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Feature flags are not supported in server-side mode
|
|
63
|
+
*/
|
|
64
|
+
reloadFeatureFlags() {
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* User identification on the server-side
|
|
68
|
+
*/
|
|
69
|
+
identify(distinctId, properties) {
|
|
70
|
+
if (this.disabled) return;
|
|
71
|
+
this.sendToPostHog("$identify", {
|
|
72
|
+
$set: properties ?? {},
|
|
73
|
+
distinct_id: distinctId
|
|
74
|
+
}).catch((_error) => {
|
|
75
|
+
if (this.config.debug) {
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Reset is not applicable on server-side
|
|
81
|
+
*/
|
|
82
|
+
reset() {
|
|
83
|
+
}
|
|
84
|
+
isEnabled() {
|
|
85
|
+
return !this.disabled;
|
|
86
|
+
}
|
|
87
|
+
getInstallationId() {
|
|
88
|
+
return this.config.installationId;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Send event to PostHog using fetch API
|
|
92
|
+
* Note: API key authentication is handled by the Cloudflare Worker proxy,
|
|
93
|
+
* so we don't include it in the payload.
|
|
94
|
+
*/
|
|
95
|
+
async sendToPostHog(event, properties) {
|
|
96
|
+
if (this.disabled) return;
|
|
97
|
+
const payload = {
|
|
98
|
+
event,
|
|
99
|
+
properties: {
|
|
100
|
+
...properties,
|
|
101
|
+
installation_id: this.config.installationId
|
|
102
|
+
},
|
|
103
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
104
|
+
};
|
|
105
|
+
try {
|
|
106
|
+
const response = await fetch(`${this.config.apiHost}/capture`, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: {
|
|
109
|
+
"Content-Type": "application/json"
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify(payload),
|
|
112
|
+
// Use keepalive for reliability
|
|
113
|
+
keepalive: true
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
throw new Error(`PostHog API returned ${response.status}: ${response.statusText}`);
|
|
117
|
+
}
|
|
118
|
+
} catch (_error) {
|
|
119
|
+
if (this.config.debug) {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
var serverAnalyticsInstance = null;
|
|
125
|
+
function initServerAnalytics(config) {
|
|
126
|
+
if (!serverAnalyticsInstance) {
|
|
127
|
+
serverAnalyticsInstance = new ServerAnalytics(config);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function getServerAnalytics() {
|
|
131
|
+
if (!serverAnalyticsInstance) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"Server analytics not initialized. Call initServerAnalytics() first (e.g., in your root layout or middleware)."
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return serverAnalyticsInstance;
|
|
137
|
+
}
|
|
138
|
+
var serverAnalytics = new Proxy({}, {
|
|
139
|
+
get(_target, prop) {
|
|
140
|
+
if (!serverAnalyticsInstance) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
"Server analytics not initialized. Call initServerAnalytics({ installationId: '...' }) first (e.g., in your root layout or middleware)."
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return serverAnalyticsInstance[prop];
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
export {
|
|
149
|
+
getServerAnalytics,
|
|
150
|
+
initServerAnalytics,
|
|
151
|
+
serverAnalytics
|
|
152
|
+
};
|
|
153
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import { mergeConfig } from \"./config\";\nimport type { Analytics, AnalyticsConfig, ErrorProperties, EventProperties, EventType } from \"./types\";\nimport { ensureError } from \"./utils\";\n\n/**\n * Server-side analytics implementation\n * This uses PostHog's API directly for server-side tracking\n */\nclass ServerAnalytics implements Analytics {\n\tprivate config: Required<AnalyticsConfig>;\n\tprivate disabled: boolean;\n\n\tconstructor(config: AnalyticsConfig) {\n\t\tthis.config = mergeConfig(config);\n\t\tthis.disabled = this.config.disabled;\n\t}\n\n\t/**\n\t * Track an event on the server-side\n\t */\n\ttrackEvent(eventType: EventType | string, properties?: EventProperties): void {\n\t\tif (this.disabled) return;\n\n\t\t// Send event to PostHog using fetch\n\t\tthis.sendToPostHog(eventType, {\n\t\t\t...properties,\n\t\t\t...(properties?.metadata ?? {}),\n\t\t}).catch((_error) => {\n\t\t\tif (this.config.debug) {\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Track an error on the server-side\n\t */\n\ttrackError(error: Error, additionalProperties?: EventProperties): void {\n\t\tif (this.disabled) return;\n\n\t\tconst errorObj = ensureError(error);\n\t\tconst errorProperties: ErrorProperties = {\n\t\t\tmessage: errorObj.message,\n\t\t\tname: errorObj.name,\n\t\t\tstack: errorObj.stack,\n\t\t\tcause: errorObj.cause ? String(errorObj.cause) : undefined,\n\t\t\t...additionalProperties,\n\t\t};\n\n\t\tthis.sendToPostHog(\"error\", {\n\t\t\t...errorProperties,\n\t\t\t...(additionalProperties?.metadata ?? {}),\n\t\t}).catch((_error) => {\n\t\t\tif (this.config.debug) {\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Feature flags are not supported in server-side mode\n\t * Use client-side hooks or PostHog API directly for feature flags\n\t */\n\tisFeatureEnabled(_flagKey: string): boolean {\n\t\treturn false;\n\t}\n\n\t/**\n\t * Feature flags are not supported in server-side mode\n\t */\n\tgetFeatureFlag(_flagKey: string): string | boolean | undefined {\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Feature flags are not supported in server-side mode\n\t */\n\treloadFeatureFlags(): void {}\n\n\t/**\n\t * User identification on the server-side\n\t */\n\tidentify(distinctId: string, properties?: Record<string, unknown>): void {\n\t\tif (this.disabled) return;\n\n\t\tthis.sendToPostHog(\"$identify\", {\n\t\t\t$set: properties ?? {},\n\t\t\tdistinct_id: distinctId,\n\t\t}).catch((_error) => {\n\t\t\tif (this.config.debug) {\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Reset is not applicable on server-side\n\t */\n\treset(): void {}\n\n\tisEnabled(): boolean {\n\t\treturn !this.disabled;\n\t}\n\n\tgetInstallationId(): string {\n\t\treturn this.config.installationId;\n\t}\n\n\t/**\n\t * Send event to PostHog using fetch API\n\t * Note: API key authentication is handled by the Cloudflare Worker proxy,\n\t * so we don't include it in the payload.\n\t */\n\tprivate async sendToPostHog(event: string, properties: Record<string, unknown>): Promise<void> {\n\t\tif (this.disabled) return;\n\n\t\tconst payload = {\n\t\t\tevent,\n\t\t\tproperties: {\n\t\t\t\t...properties,\n\t\t\t\tinstallation_id: this.config.installationId,\n\t\t\t},\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t};\n\n\t\ttry {\n\t\t\tconst response = await fetch(`${this.config.apiHost}/capture`, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t// Use keepalive for reliability\n\t\t\t\tkeepalive: true,\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`PostHog API returned ${response.status}: ${response.statusText}`);\n\t\t\t}\n\t\t} catch (_error) {\n\t\t\t// Silently fail - we don't want analytics errors to break the app\n\t\t\tif (this.config.debug) {\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Global server-side analytics instance\n */\nlet serverAnalyticsInstance: ServerAnalytics | null = null;\n\n/**\n * Initialize server-side analytics\n * Call this once in your app (e.g., in a layout or middleware)\n *\n * @example\n * ```ts\n * // In your Next.js layout or API route\n * import { initServerAnalytics } from '@codaco/analytics/server';\n *\n * initServerAnalytics({\n * installationId: 'your-unique-installation-id',\n * });\n * ```\n */\nexport function initServerAnalytics(config: AnalyticsConfig): void {\n\tif (!serverAnalyticsInstance) {\n\t\tserverAnalyticsInstance = new ServerAnalytics(config);\n\t}\n}\n\n/**\n * Get the server-side analytics instance\n * Use this in server components, API routes, and server actions\n *\n * @example\n * ```ts\n * import { getServerAnalytics } from '@codaco/analytics/server';\n *\n * export async function POST(request: Request) {\n * const analytics = getServerAnalytics();\n * analytics.trackEvent('data_exported', {\n * metadata: { format: 'csv' }\n * });\n *\n * // ... rest of your handler\n * }\n * ```\n */\nexport function getServerAnalytics(): Analytics {\n\tif (!serverAnalyticsInstance) {\n\t\tthrow new Error(\n\t\t\t\"Server analytics not initialized. Call initServerAnalytics() first (e.g., in your root layout or middleware).\",\n\t\t);\n\t}\n\n\treturn serverAnalyticsInstance;\n}\n\n/**\n * Convenience export for direct usage\n * Requires calling initServerAnalytics() first\n */\nexport const serverAnalytics = new Proxy({} as Analytics, {\n\tget(_target, prop) {\n\t\tif (!serverAnalyticsInstance) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Server analytics not initialized. Call initServerAnalytics({ installationId: '...' }) first \" +\n\t\t\t\t\t\"(e.g., in your root layout or middleware).\",\n\t\t\t);\n\t\t}\n\n\t\treturn serverAnalyticsInstance[prop as keyof Analytics];\n\t},\n});\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Event types supported by the analytics system.
|
|
5
|
+
* These are converted to snake_case for PostHog.
|
|
6
|
+
*/
|
|
7
|
+
declare const eventTypes: readonly ["app_setup", "protocol_installed", "interview_started", "interview_completed", "data_exported", "error"];
|
|
8
|
+
type EventType = (typeof eventTypes)[number];
|
|
9
|
+
/**
|
|
10
|
+
* Legacy event type mapping for backward compatibility
|
|
11
|
+
*/
|
|
12
|
+
declare const legacyEventTypeMap: Record<string, EventType>;
|
|
13
|
+
/**
|
|
14
|
+
* Standard event properties that can be sent with any event
|
|
15
|
+
*/
|
|
16
|
+
declare const EventPropertiesSchema: z.ZodObject<{
|
|
17
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
type EventProperties = z.infer<typeof EventPropertiesSchema>;
|
|
20
|
+
/**
|
|
21
|
+
* Error-specific properties for error tracking
|
|
22
|
+
*/
|
|
23
|
+
declare const ErrorPropertiesSchema: z.ZodObject<{
|
|
24
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
25
|
+
message: z.ZodString;
|
|
26
|
+
name: z.ZodString;
|
|
27
|
+
stack: z.ZodOptional<z.ZodString>;
|
|
28
|
+
cause: z.ZodOptional<z.ZodString>;
|
|
29
|
+
}, z.core.$strip>;
|
|
30
|
+
type ErrorProperties = z.infer<typeof ErrorPropertiesSchema>;
|
|
31
|
+
/**
|
|
32
|
+
* Analytics configuration options
|
|
33
|
+
*
|
|
34
|
+
* This package is designed to work exclusively with the Cloudflare Worker
|
|
35
|
+
* reverse proxy at ph-relay.networkcanvas.com. All authentication is handled
|
|
36
|
+
* by the worker, so the API key is optional.
|
|
37
|
+
*/
|
|
38
|
+
type AnalyticsConfig = {
|
|
39
|
+
/**
|
|
40
|
+
* PostHog API host - should point to the Cloudflare Worker reverse proxy
|
|
41
|
+
* Defaults to "https://ph-relay.networkcanvas.com"
|
|
42
|
+
*/
|
|
43
|
+
apiHost?: string;
|
|
44
|
+
/**
|
|
45
|
+
* PostHog project API key (optional)
|
|
46
|
+
*
|
|
47
|
+
* When using the reverse proxy (default), authentication is handled by the
|
|
48
|
+
* Cloudflare Worker. A placeholder key will be used for client-side PostHog
|
|
49
|
+
* initialization if not provided.
|
|
50
|
+
*
|
|
51
|
+
* Only set this if you need to override the default behavior.
|
|
52
|
+
*/
|
|
53
|
+
apiKey?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Unique identifier for this installation/deployment
|
|
56
|
+
* This is included with every event as a super property
|
|
57
|
+
*/
|
|
58
|
+
installationId: string;
|
|
59
|
+
/**
|
|
60
|
+
* Disable all analytics tracking
|
|
61
|
+
* Can be set via DISABLE_ANALYTICS or NEXT_PUBLIC_DISABLE_ANALYTICS env var
|
|
62
|
+
*/
|
|
63
|
+
disabled?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Enable debug mode for PostHog
|
|
66
|
+
*/
|
|
67
|
+
debug?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Additional options to pass to PostHog initialization
|
|
70
|
+
*/
|
|
71
|
+
posthogOptions?: {
|
|
72
|
+
/**
|
|
73
|
+
* Disable session recording
|
|
74
|
+
*/
|
|
75
|
+
disable_session_recording?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Autocapture settings
|
|
78
|
+
*/
|
|
79
|
+
autocapture?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Capture pageviews automatically
|
|
82
|
+
*/
|
|
83
|
+
capture_pageview?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Capture pageleave events
|
|
86
|
+
*/
|
|
87
|
+
capture_pageleave?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Cross-subdomain cookie
|
|
90
|
+
*/
|
|
91
|
+
cross_subdomain_cookie?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Advanced feature flags support
|
|
94
|
+
*/
|
|
95
|
+
advanced_disable_feature_flags?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Other PostHog options
|
|
98
|
+
*/
|
|
99
|
+
[key: string]: unknown;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Analytics instance interface
|
|
104
|
+
*/
|
|
105
|
+
type Analytics = {
|
|
106
|
+
/**
|
|
107
|
+
* Track a custom event
|
|
108
|
+
*/
|
|
109
|
+
trackEvent: (eventType: EventType | string, properties?: EventProperties) => void;
|
|
110
|
+
/**
|
|
111
|
+
* Track an error with full stack trace
|
|
112
|
+
*/
|
|
113
|
+
trackError: (error: Error, additionalProperties?: EventProperties) => void;
|
|
114
|
+
/**
|
|
115
|
+
* Check if a feature flag is enabled
|
|
116
|
+
*/
|
|
117
|
+
isFeatureEnabled: (flagKey: string) => boolean | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Get the value of a feature flag
|
|
120
|
+
*/
|
|
121
|
+
getFeatureFlag: (flagKey: string) => string | boolean | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* Reload feature flags from PostHog
|
|
124
|
+
*/
|
|
125
|
+
reloadFeatureFlags: () => void;
|
|
126
|
+
/**
|
|
127
|
+
* Identify a user (optional - for advanced use cases)
|
|
128
|
+
* Note: By default we only track installations, not users
|
|
129
|
+
*/
|
|
130
|
+
identify: (distinctId: string, properties?: Record<string, unknown>) => void;
|
|
131
|
+
/**
|
|
132
|
+
* Reset the user identity
|
|
133
|
+
*/
|
|
134
|
+
reset: () => void;
|
|
135
|
+
/**
|
|
136
|
+
* Check if analytics is enabled
|
|
137
|
+
*/
|
|
138
|
+
isEnabled: () => boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Get the installation ID
|
|
141
|
+
*/
|
|
142
|
+
getInstallationId: () => string;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export { type AnalyticsConfig as A, type ErrorProperties as E, type Analytics as a, type EventProperties as b, type EventType as c, eventTypes as e, legacyEventTypeMap as l };
|
package/package.json
CHANGED
|
@@ -1,25 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codaco/analytics",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./server": {
|
|
13
|
+
"types": "./dist/server.d.ts",
|
|
14
|
+
"import": "./dist/server.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
7
17
|
"author": "Complex Data Collective <hello@complexdatacollective.org>",
|
|
8
|
-
"description": "
|
|
18
|
+
"description": "PostHog analytics wrapper for Network Canvas applications with installation ID tracking and error reporting",
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"tag": "alpha"
|
|
21
|
+
},
|
|
9
22
|
"peerDependencies": {
|
|
10
|
-
"next": "
|
|
23
|
+
"next": "^16.0.7",
|
|
24
|
+
"react": "^19.2.0"
|
|
11
25
|
},
|
|
12
26
|
"devDependencies": {
|
|
13
|
-
"
|
|
14
|
-
"
|
|
27
|
+
"@types/node": "^24.10.1",
|
|
28
|
+
"@types/react": "^19.2.7",
|
|
29
|
+
"tsup": "^8.5.1",
|
|
30
|
+
"typescript": "^5.9.3",
|
|
31
|
+
"vitest": "^4.0.13",
|
|
15
32
|
"@codaco/tsconfig": "0.1.0"
|
|
16
33
|
},
|
|
17
34
|
"dependencies": {
|
|
18
|
-
"
|
|
35
|
+
"posthog-js": "^1.304.0",
|
|
36
|
+
"zod": "^4.1.13"
|
|
19
37
|
},
|
|
20
38
|
"scripts": {
|
|
21
|
-
"build": "tsup src/index.ts --format esm --dts --clean --sourcemap",
|
|
39
|
+
"build": "tsup src/index.ts src/server.ts --format esm --dts --clean --sourcemap",
|
|
22
40
|
"dev": "npm run build -- --watch",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest",
|
|
23
43
|
"typecheck": "tsc --noEmit"
|
|
24
44
|
}
|
|
25
45
|
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createAnalytics } from "../client";
|
|
3
|
+
import type { AnalyticsConfig } from "../types";
|
|
4
|
+
|
|
5
|
+
// Mock posthog-js
|
|
6
|
+
vi.mock("posthog-js", () => ({
|
|
7
|
+
default: {
|
|
8
|
+
init: vi.fn().mockReturnValue({
|
|
9
|
+
register: vi.fn(),
|
|
10
|
+
debug: vi.fn(),
|
|
11
|
+
}),
|
|
12
|
+
capture: vi.fn(),
|
|
13
|
+
isFeatureEnabled: vi.fn(),
|
|
14
|
+
getFeatureFlag: vi.fn(),
|
|
15
|
+
reloadFeatureFlags: vi.fn(),
|
|
16
|
+
identify: vi.fn(),
|
|
17
|
+
reset: vi.fn(),
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import posthog from "posthog-js";
|
|
22
|
+
|
|
23
|
+
describe("createAnalytics", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const mockConfig: Required<AnalyticsConfig> = {
|
|
29
|
+
apiHost: "https://ph-relay.networkcanvas.com",
|
|
30
|
+
apiKey: "phc_test",
|
|
31
|
+
installationId: "test-install-123",
|
|
32
|
+
disabled: false,
|
|
33
|
+
debug: false,
|
|
34
|
+
posthogOptions: {},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe("initialization", () => {
|
|
38
|
+
it("should initialize PostHog with correct config", () => {
|
|
39
|
+
createAnalytics(mockConfig);
|
|
40
|
+
|
|
41
|
+
expect(posthog.init).toHaveBeenCalledWith(
|
|
42
|
+
"phc_test",
|
|
43
|
+
expect.objectContaining({
|
|
44
|
+
api_host: "https://ph-relay.networkcanvas.com",
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should not initialize PostHog when disabled", () => {
|
|
50
|
+
const disabledConfig = { ...mockConfig, disabled: true };
|
|
51
|
+
const analytics = createAnalytics(disabledConfig);
|
|
52
|
+
|
|
53
|
+
expect(posthog.init).not.toHaveBeenCalled();
|
|
54
|
+
expect(analytics.isEnabled()).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should register installation ID as super property", () => {
|
|
58
|
+
const mockPosthogInstance = {
|
|
59
|
+
register: vi.fn(),
|
|
60
|
+
debug: vi.fn(),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
vi.mocked(posthog.init).mockImplementation((_token, options) => {
|
|
64
|
+
options?.loaded?.(mockPosthogInstance as never);
|
|
65
|
+
return mockPosthogInstance as never;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
createAnalytics(mockConfig);
|
|
69
|
+
|
|
70
|
+
expect(mockPosthogInstance.register).toHaveBeenCalledWith({
|
|
71
|
+
installation_id: "test-install-123",
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should enable debug mode when configured", () => {
|
|
76
|
+
const mockPosthogInstance = {
|
|
77
|
+
register: vi.fn(),
|
|
78
|
+
debug: vi.fn(),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
vi.mocked(posthog.init).mockImplementation((_token, options) => {
|
|
82
|
+
options?.loaded?.(mockPosthogInstance as never);
|
|
83
|
+
return mockPosthogInstance as never;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const debugConfig = { ...mockConfig, debug: true };
|
|
87
|
+
createAnalytics(debugConfig);
|
|
88
|
+
|
|
89
|
+
expect(mockPosthogInstance.debug).toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("trackEvent", () => {
|
|
94
|
+
it("should capture events with PostHog", () => {
|
|
95
|
+
const analytics = createAnalytics(mockConfig);
|
|
96
|
+
|
|
97
|
+
analytics.trackEvent("app_setup", {
|
|
98
|
+
metadata: { version: "1.0.0" },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(posthog.capture).toHaveBeenCalledWith("app_setup", {
|
|
102
|
+
metadata: { version: "1.0.0" },
|
|
103
|
+
version: "1.0.0",
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should flatten metadata into properties", () => {
|
|
108
|
+
const analytics = createAnalytics(mockConfig);
|
|
109
|
+
|
|
110
|
+
analytics.trackEvent("protocol_installed", {
|
|
111
|
+
metadata: {
|
|
112
|
+
protocolId: "proto-123",
|
|
113
|
+
version: "2.0.0",
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(posthog.capture).toHaveBeenCalledWith("protocol_installed", {
|
|
118
|
+
metadata: {
|
|
119
|
+
protocolId: "proto-123",
|
|
120
|
+
version: "2.0.0",
|
|
121
|
+
},
|
|
122
|
+
protocolId: "proto-123",
|
|
123
|
+
version: "2.0.0",
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should not capture events when disabled", () => {
|
|
128
|
+
const disabledConfig = { ...mockConfig, disabled: true };
|
|
129
|
+
const analytics = createAnalytics(disabledConfig);
|
|
130
|
+
|
|
131
|
+
analytics.trackEvent("app_setup");
|
|
132
|
+
|
|
133
|
+
expect(posthog.capture).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should handle errors gracefully", () => {
|
|
137
|
+
const analytics = createAnalytics(mockConfig);
|
|
138
|
+
vi.mocked(posthog.capture).mockImplementation(() => {
|
|
139
|
+
throw new Error("PostHog error");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Should not throw
|
|
143
|
+
expect(() => analytics.trackEvent("app_setup")).not.toThrow();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("trackError", () => {
|
|
148
|
+
it("should capture errors with full details", () => {
|
|
149
|
+
const analytics = createAnalytics(mockConfig);
|
|
150
|
+
const error = new Error("Test error");
|
|
151
|
+
error.stack = "Error: Test error\n at test.ts:10";
|
|
152
|
+
|
|
153
|
+
analytics.trackError(error);
|
|
154
|
+
|
|
155
|
+
expect(posthog.capture).toHaveBeenCalledWith(
|
|
156
|
+
"error",
|
|
157
|
+
expect.objectContaining({
|
|
158
|
+
message: "Test error",
|
|
159
|
+
name: "Error",
|
|
160
|
+
stack: expect.stringContaining("Error: Test error"),
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should include additional properties", () => {
|
|
166
|
+
const analytics = createAnalytics(mockConfig);
|
|
167
|
+
const error = new Error("Test error");
|
|
168
|
+
|
|
169
|
+
analytics.trackError(error, {
|
|
170
|
+
metadata: { context: "test" },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(posthog.capture).toHaveBeenCalledWith(
|
|
174
|
+
"error",
|
|
175
|
+
expect.objectContaining({
|
|
176
|
+
message: "Test error",
|
|
177
|
+
metadata: { context: "test" },
|
|
178
|
+
context: "test",
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should not capture errors when disabled", () => {
|
|
184
|
+
const disabledConfig = { ...mockConfig, disabled: true };
|
|
185
|
+
const analytics = createAnalytics(disabledConfig);
|
|
186
|
+
const error = new Error("Test error");
|
|
187
|
+
|
|
188
|
+
analytics.trackError(error);
|
|
189
|
+
|
|
190
|
+
expect(posthog.capture).not.toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("feature flags", () => {
|
|
195
|
+
it("should check if feature is enabled", () => {
|
|
196
|
+
const analytics = createAnalytics(mockConfig);
|
|
197
|
+
vi.mocked(posthog.isFeatureEnabled).mockReturnValue(true);
|
|
198
|
+
|
|
199
|
+
const result = analytics.isFeatureEnabled("new-feature");
|
|
200
|
+
|
|
201
|
+
expect(result).toBe(true);
|
|
202
|
+
expect(posthog.isFeatureEnabled).toHaveBeenCalledWith("new-feature");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should get feature flag value", () => {
|
|
206
|
+
const analytics = createAnalytics(mockConfig);
|
|
207
|
+
vi.mocked(posthog.getFeatureFlag).mockReturnValue("variant-a");
|
|
208
|
+
|
|
209
|
+
const result = analytics.getFeatureFlag("experiment");
|
|
210
|
+
|
|
211
|
+
expect(result).toBe("variant-a");
|
|
212
|
+
expect(posthog.getFeatureFlag).toHaveBeenCalledWith("experiment");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should reload feature flags", () => {
|
|
216
|
+
const analytics = createAnalytics(mockConfig);
|
|
217
|
+
|
|
218
|
+
analytics.reloadFeatureFlags();
|
|
219
|
+
|
|
220
|
+
expect(posthog.reloadFeatureFlags).toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should return false for feature flags when disabled", () => {
|
|
224
|
+
const disabledConfig = { ...mockConfig, disabled: true };
|
|
225
|
+
const analytics = createAnalytics(disabledConfig);
|
|
226
|
+
|
|
227
|
+
expect(analytics.isFeatureEnabled("test")).toBe(false);
|
|
228
|
+
expect(analytics.getFeatureFlag("test")).toBeUndefined();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("user identification", () => {
|
|
233
|
+
it("should identify users", () => {
|
|
234
|
+
const analytics = createAnalytics(mockConfig);
|
|
235
|
+
|
|
236
|
+
analytics.identify("user-123", { email: "test@example.com" });
|
|
237
|
+
|
|
238
|
+
expect(posthog.identify).toHaveBeenCalledWith("user-123", {
|
|
239
|
+
email: "test@example.com",
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should reset user identity", () => {
|
|
244
|
+
const analytics = createAnalytics(mockConfig);
|
|
245
|
+
|
|
246
|
+
analytics.reset();
|
|
247
|
+
|
|
248
|
+
expect(posthog.reset).toHaveBeenCalled();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("should not identify when disabled", () => {
|
|
252
|
+
const disabledConfig = { ...mockConfig, disabled: true };
|
|
253
|
+
const analytics = createAnalytics(disabledConfig);
|
|
254
|
+
|
|
255
|
+
analytics.identify("user-123");
|
|
256
|
+
|
|
257
|
+
expect(posthog.identify).not.toHaveBeenCalled();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("utility methods", () => {
|
|
262
|
+
it("should return enabled status", () => {
|
|
263
|
+
const analytics = createAnalytics(mockConfig);
|
|
264
|
+
expect(analytics.isEnabled()).toBe(true);
|
|
265
|
+
|
|
266
|
+
const disabledConfig = { ...mockConfig, disabled: true };
|
|
267
|
+
const disabledAnalytics = createAnalytics(disabledConfig);
|
|
268
|
+
expect(disabledAnalytics.isEnabled()).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("should return installation ID", () => {
|
|
272
|
+
const analytics = createAnalytics(mockConfig);
|
|
273
|
+
expect(analytics.getInstallationId()).toBe("test-install-123");
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
});
|