@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/index.d.ts
CHANGED
|
@@ -1,88 +1,119 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { A as AnalyticsConfig, a as Analytics } from './types-DK5BiTnW.js';
|
|
2
|
+
export { E as ErrorProperties, b as EventProperties, c as EventType, e as eventTypes, l as legacyEventTypeMap } from './types-DK5BiTnW.js';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
5
|
+
import 'zod';
|
|
3
6
|
|
|
4
|
-
declare const eventTypes: readonly ["AppSetup", "ProtocolInstalled", "InterviewStarted", "InterviewCompleted", "DataExported"];
|
|
5
7
|
/**
|
|
6
|
-
*
|
|
7
|
-
* general events or errors. We discriminate on the `type` property to determine
|
|
8
|
-
* which schema to use, and then merge the shared properties.
|
|
8
|
+
* Check if analytics is disabled via environment variables
|
|
9
9
|
*/
|
|
10
|
-
declare
|
|
11
|
-
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
12
|
-
type: z.ZodEnum<{
|
|
13
|
-
AppSetup: "AppSetup";
|
|
14
|
-
ProtocolInstalled: "ProtocolInstalled";
|
|
15
|
-
InterviewStarted: "InterviewStarted";
|
|
16
|
-
InterviewCompleted: "InterviewCompleted";
|
|
17
|
-
DataExported: "DataExported";
|
|
18
|
-
}>;
|
|
19
|
-
}, z.core.$strip>, z.ZodObject<{
|
|
20
|
-
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
21
|
-
type: z.ZodLiteral<"Error">;
|
|
22
|
-
message: z.ZodString;
|
|
23
|
-
name: z.ZodString;
|
|
24
|
-
stack: z.ZodOptional<z.ZodString>;
|
|
25
|
-
cause: z.ZodOptional<z.ZodString>;
|
|
26
|
-
}, z.core.$strip>], "type">;
|
|
27
|
-
type RawEvent = z.infer<typeof RawEventSchema>;
|
|
28
|
-
declare const TrackableEventSchema: z.ZodIntersection<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
29
|
-
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
30
|
-
type: z.ZodEnum<{
|
|
31
|
-
AppSetup: "AppSetup";
|
|
32
|
-
ProtocolInstalled: "ProtocolInstalled";
|
|
33
|
-
InterviewStarted: "InterviewStarted";
|
|
34
|
-
InterviewCompleted: "InterviewCompleted";
|
|
35
|
-
DataExported: "DataExported";
|
|
36
|
-
}>;
|
|
37
|
-
}, z.core.$strip>, z.ZodObject<{
|
|
38
|
-
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
39
|
-
type: z.ZodLiteral<"Error">;
|
|
40
|
-
message: z.ZodString;
|
|
41
|
-
name: z.ZodString;
|
|
42
|
-
stack: z.ZodOptional<z.ZodString>;
|
|
43
|
-
cause: z.ZodOptional<z.ZodString>;
|
|
44
|
-
}, z.core.$strip>], "type">, z.ZodObject<{
|
|
45
|
-
timestamp: z.ZodString;
|
|
46
|
-
}, z.core.$strip>>;
|
|
47
|
-
type TrackableEvent = z.infer<typeof TrackableEventSchema>;
|
|
10
|
+
declare function isDisabledByEnv(): boolean;
|
|
48
11
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* intersection of the trackable event and the dispatchable properties.
|
|
12
|
+
* Default configuration for analytics
|
|
13
|
+
* API host and key are hardcoded, but disabled flag can be set via environment variables
|
|
52
14
|
*/
|
|
53
|
-
declare const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
15
|
+
declare const defaultConfig: Partial<AnalyticsConfig>;
|
|
16
|
+
/**
|
|
17
|
+
* Merge user config with defaults
|
|
18
|
+
*
|
|
19
|
+
* Note: This package is designed to work exclusively with the Cloudflare Worker
|
|
20
|
+
* reverse proxy (ph-relay.networkcanvas.com). Authentication is handled by the
|
|
21
|
+
* worker, so the API key is optional and defaults to a placeholder value.
|
|
22
|
+
*
|
|
23
|
+
* The only environment variable checked is DISABLE_ANALYTICS / NEXT_PUBLIC_DISABLE_ANALYTICS
|
|
24
|
+
* for disabling tracking. All other configuration is hardcoded or passed explicitly.
|
|
25
|
+
*/
|
|
26
|
+
declare function mergeConfig(userConfig: AnalyticsConfig): Required<AnalyticsConfig>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Hook to access analytics functionality in React components
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* import { useAnalytics } from '@codaco/analytics';
|
|
34
|
+
*
|
|
35
|
+
* function MyComponent() {
|
|
36
|
+
* const { trackEvent, trackError } = useAnalytics();
|
|
37
|
+
*
|
|
38
|
+
* const handleAction = () => {
|
|
39
|
+
* trackEvent('protocol_installed', {
|
|
40
|
+
* metadata: { protocolName: 'My Protocol' }
|
|
41
|
+
* });
|
|
42
|
+
* };
|
|
43
|
+
*
|
|
44
|
+
* return <button onClick={handleAction}>Install Protocol</button>;
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
declare function useAnalytics(): Analytics;
|
|
49
|
+
/**
|
|
50
|
+
* Hook to access feature flags
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* import { useFeatureFlag } from '@codaco/analytics';
|
|
55
|
+
*
|
|
56
|
+
* function MyComponent() {
|
|
57
|
+
* const isNewFeatureEnabled = useFeatureFlag('new-feature');
|
|
58
|
+
*
|
|
59
|
+
* if (isNewFeatureEnabled) {
|
|
60
|
+
* return <NewFeature />;
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* return <OldFeature />;
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare function useFeatureFlag(flagKey: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Hook to access feature flag values (for multivariate flags)
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```tsx
|
|
73
|
+
* import { useFeatureFlagValue } from '@codaco/analytics';
|
|
74
|
+
*
|
|
75
|
+
* function MyComponent() {
|
|
76
|
+
* const theme = useFeatureFlagValue('theme-variant');
|
|
77
|
+
*
|
|
78
|
+
* return <div className={theme === 'dark' ? 'dark-theme' : 'light-theme'}>
|
|
79
|
+
* Content
|
|
80
|
+
* </div>;
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
declare function useFeatureFlagValue(flagKey: string): string | boolean | undefined;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Props for the AnalyticsProvider
|
|
88
|
+
*/
|
|
89
|
+
type AnalyticsProviderProps = {
|
|
90
|
+
children: ReactNode;
|
|
91
|
+
config: AnalyticsConfig;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Provider component that initializes PostHog and provides analytics context
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```tsx
|
|
98
|
+
* import { AnalyticsProvider } from '@codaco/analytics';
|
|
99
|
+
*
|
|
100
|
+
* function App({ children }) {
|
|
101
|
+
* return (
|
|
102
|
+
* <AnalyticsProvider
|
|
103
|
+
* config={{
|
|
104
|
+
* installationId: 'your-installation-id',
|
|
105
|
+
* apiKey: 'phc_your_api_key', // optional if set via env
|
|
106
|
+
* apiHost: 'https://ph-relay.networkcanvas.com', // optional
|
|
107
|
+
* }}
|
|
108
|
+
* >
|
|
109
|
+
* {children}
|
|
110
|
+
* </AnalyticsProvider>
|
|
111
|
+
* );
|
|
112
|
+
* }
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
declare function AnalyticsProvider({ children, config }: AnalyticsProviderProps): react.JSX.Element | null;
|
|
116
|
+
|
|
117
|
+
declare function ensureError(value: unknown): Error;
|
|
87
118
|
|
|
88
|
-
export {
|
|
119
|
+
export { Analytics, AnalyticsConfig, AnalyticsProvider, type AnalyticsProviderProps, defaultConfig, ensureError, isDisabledByEnv, mergeConfig, useAnalytics, useFeatureFlag, useFeatureFlagValue };
|
package/dist/index.js
CHANGED
|
@@ -1,182 +1,210 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
defaultConfig,
|
|
3
|
+
ensureError,
|
|
4
|
+
isDisabledByEnv,
|
|
5
|
+
mergeConfig
|
|
6
|
+
} from "./chunk-3NEQVIC4.js";
|
|
4
7
|
|
|
5
|
-
// src/
|
|
6
|
-
|
|
7
|
-
if (!value) return new Error("No value was thrown");
|
|
8
|
-
if (value instanceof Error) return value;
|
|
9
|
-
if (Object.prototype.isPrototypeOf.call(value, Error)) return value;
|
|
10
|
-
let stringified = "[Unable to stringify the thrown value]";
|
|
11
|
-
try {
|
|
12
|
-
stringified = JSON.stringify(value);
|
|
13
|
-
} catch (e) {
|
|
14
|
-
console.error(e);
|
|
15
|
-
}
|
|
16
|
-
const error = new Error(`This value was thrown as is, not through an Error: ${stringified}`);
|
|
17
|
-
return error;
|
|
18
|
-
}
|
|
8
|
+
// src/hooks.ts
|
|
9
|
+
import { useContext } from "react";
|
|
19
10
|
|
|
20
|
-
// src/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
var AnalyticsEventSchema = z.intersection(TrackableEventSchema, DispatchablePropertiesSchema);
|
|
54
|
-
var createRouteHandler = ({
|
|
55
|
-
platformUrl = "https://analytics.networkcanvas.com",
|
|
56
|
-
installationId,
|
|
57
|
-
disableAnalytics
|
|
58
|
-
}) => {
|
|
59
|
-
return async (request) => {
|
|
60
|
-
try {
|
|
61
|
-
const incomingEvent = await request.json();
|
|
62
|
-
if (disableAnalytics) {
|
|
63
|
-
console.info("\u{1F6D1} Analytics disabled. Payload not sent.");
|
|
64
|
-
try {
|
|
65
|
-
console.info("Payload:", "\n", JSON.stringify(incomingEvent, null, 2));
|
|
66
|
-
} catch (e) {
|
|
67
|
-
console.error("Error stringifying payload:", e);
|
|
11
|
+
// src/provider.tsx
|
|
12
|
+
import { createContext, useEffect, useRef } from "react";
|
|
13
|
+
|
|
14
|
+
// src/client.ts
|
|
15
|
+
import posthog from "posthog-js";
|
|
16
|
+
function createAnalytics(config) {
|
|
17
|
+
const { apiHost, apiKey, installationId, disabled, debug, posthogOptions } = config;
|
|
18
|
+
if (disabled) {
|
|
19
|
+
return createNoOpAnalytics(installationId);
|
|
20
|
+
}
|
|
21
|
+
posthog.init(apiKey, {
|
|
22
|
+
api_host: apiHost,
|
|
23
|
+
loaded: (posthogInstance) => {
|
|
24
|
+
posthogInstance.register({
|
|
25
|
+
installation_id: installationId
|
|
26
|
+
});
|
|
27
|
+
if (debug) {
|
|
28
|
+
posthogInstance.debug();
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
...posthogOptions
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
trackEvent: (eventType, properties) => {
|
|
35
|
+
if (disabled) return;
|
|
36
|
+
try {
|
|
37
|
+
posthog.capture(eventType, {
|
|
38
|
+
...properties,
|
|
39
|
+
// Flatten metadata into properties for better PostHog integration
|
|
40
|
+
...properties?.metadata ?? {}
|
|
41
|
+
});
|
|
42
|
+
} catch (_e) {
|
|
43
|
+
if (debug) {
|
|
68
44
|
}
|
|
69
|
-
return NextResponse.json({ message: "Analytics disabled" }, { status: 200 });
|
|
70
45
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
46
|
+
},
|
|
47
|
+
trackError: (error, additionalProperties) => {
|
|
48
|
+
if (disabled) return;
|
|
49
|
+
try {
|
|
50
|
+
const errorObj = ensureError(error);
|
|
51
|
+
const errorProperties = {
|
|
52
|
+
message: errorObj.message,
|
|
53
|
+
name: errorObj.name,
|
|
54
|
+
stack: errorObj.stack,
|
|
55
|
+
cause: errorObj.cause ? String(errorObj.cause) : void 0,
|
|
56
|
+
...additionalProperties
|
|
57
|
+
};
|
|
58
|
+
posthog.capture("error", {
|
|
59
|
+
...errorProperties,
|
|
60
|
+
// Flatten metadata
|
|
61
|
+
...additionalProperties?.metadata ?? {}
|
|
62
|
+
});
|
|
63
|
+
} catch (_e) {
|
|
64
|
+
if (debug) {
|
|
65
|
+
}
|
|
75
66
|
}
|
|
76
|
-
|
|
67
|
+
},
|
|
68
|
+
isFeatureEnabled: (flagKey) => {
|
|
69
|
+
if (disabled) return false;
|
|
77
70
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
return posthog.isFeatureEnabled(flagKey);
|
|
72
|
+
} catch (_e) {
|
|
73
|
+
if (debug) {
|
|
81
74
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
getFeatureFlag: (flagKey) => {
|
|
79
|
+
if (disabled) return void 0;
|
|
80
|
+
try {
|
|
81
|
+
return posthog.getFeatureFlag(flagKey);
|
|
82
|
+
} catch (_e) {
|
|
83
|
+
if (debug) {
|
|
87
84
|
}
|
|
88
|
-
|
|
89
|
-
console.error("Geolocation failed:", e);
|
|
85
|
+
return void 0;
|
|
90
86
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
method: "POST",
|
|
99
|
-
headers: {
|
|
100
|
-
"Content-Type": "application/json"
|
|
101
|
-
},
|
|
102
|
-
body: JSON.stringify(analyticsEvent)
|
|
103
|
-
});
|
|
104
|
-
if (!response.ok) {
|
|
105
|
-
let error = `Analytics platform returned an unexpected error: ${response.statusText}`;
|
|
106
|
-
if (response.status === 400) {
|
|
107
|
-
error = "Analytics platform rejected the event as invalid. Please check the event schema";
|
|
87
|
+
},
|
|
88
|
+
reloadFeatureFlags: () => {
|
|
89
|
+
if (disabled) return;
|
|
90
|
+
try {
|
|
91
|
+
posthog.reloadFeatureFlags();
|
|
92
|
+
} catch (_e) {
|
|
93
|
+
if (debug) {
|
|
108
94
|
}
|
|
109
|
-
|
|
110
|
-
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
identify: (distinctId, properties) => {
|
|
98
|
+
if (disabled) return;
|
|
99
|
+
try {
|
|
100
|
+
posthog.identify(distinctId, properties);
|
|
101
|
+
} catch (_e) {
|
|
102
|
+
if (debug) {
|
|
111
103
|
}
|
|
112
|
-
|
|
113
|
-
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
reset: () => {
|
|
107
|
+
if (disabled) return;
|
|
108
|
+
try {
|
|
109
|
+
posthog.reset();
|
|
110
|
+
} catch (_e) {
|
|
111
|
+
if (debug) {
|
|
114
112
|
}
|
|
115
|
-
console.info(`\u26A0\uFE0F Analytics platform rejected event: ${error}`);
|
|
116
|
-
return Response.json(
|
|
117
|
-
{
|
|
118
|
-
error
|
|
119
|
-
},
|
|
120
|
-
{ status: 500 }
|
|
121
|
-
);
|
|
122
113
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const error = ensureError(e);
|
|
127
|
-
console.info("\u{1F6AB} Internal error with sending analytics event.");
|
|
128
|
-
return Response.json({ error: `Error in analytics route handler: ${error.message}` }, { status: 500 });
|
|
129
|
-
}
|
|
114
|
+
},
|
|
115
|
+
isEnabled: () => !disabled,
|
|
116
|
+
getInstallationId: () => installationId
|
|
130
117
|
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
118
|
+
}
|
|
119
|
+
function createNoOpAnalytics(installationId) {
|
|
120
|
+
return {
|
|
121
|
+
trackEvent: () => {
|
|
122
|
+
},
|
|
123
|
+
trackError: () => {
|
|
124
|
+
},
|
|
125
|
+
isFeatureEnabled: () => false,
|
|
126
|
+
getFeatureFlag: () => void 0,
|
|
127
|
+
reloadFeatureFlags: () => {
|
|
128
|
+
},
|
|
129
|
+
identify: () => {
|
|
130
|
+
},
|
|
131
|
+
reset: () => {
|
|
132
|
+
},
|
|
133
|
+
isEnabled: () => false,
|
|
134
|
+
getInstallationId: () => installationId
|
|
137
135
|
};
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (response.status === 404) {
|
|
149
|
-
return {
|
|
150
|
-
error: "Analytics endpoint not found, did you forget to add the route?",
|
|
151
|
-
success: false
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
if (response.status === 400) {
|
|
155
|
-
return {
|
|
156
|
-
error: `Invalid event sent to analytics endpoint: ${response.statusText}`,
|
|
157
|
-
success: false
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
error: `Internal server error when sending analytics event: ${response.statusText}. Check the route handler implementation.`,
|
|
162
|
-
success: false
|
|
163
|
-
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/provider.tsx
|
|
139
|
+
var AnalyticsContext = createContext(null);
|
|
140
|
+
function AnalyticsProvider({ children, config }) {
|
|
141
|
+
const analyticsRef = useRef(null);
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!analyticsRef.current) {
|
|
144
|
+
const mergedConfig = mergeConfig(config);
|
|
145
|
+
analyticsRef.current = createAnalytics(mergedConfig);
|
|
164
146
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
147
|
+
}, []);
|
|
148
|
+
if (!analyticsRef.current) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return /* @__PURE__ */ React.createElement(AnalyticsContext.Provider, { value: analyticsRef.current }, children);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/hooks.ts
|
|
155
|
+
function useAnalytics() {
|
|
156
|
+
const analytics = useContext(AnalyticsContext);
|
|
157
|
+
if (!analytics) {
|
|
158
|
+
throw new Error("useAnalytics must be used within an AnalyticsProvider");
|
|
172
159
|
}
|
|
160
|
+
return analytics;
|
|
161
|
+
}
|
|
162
|
+
function useFeatureFlag(flagKey) {
|
|
163
|
+
const analytics = useAnalytics();
|
|
164
|
+
return analytics.isFeatureEnabled(flagKey) ?? false;
|
|
165
|
+
}
|
|
166
|
+
function useFeatureFlagValue(flagKey) {
|
|
167
|
+
const analytics = useAnalytics();
|
|
168
|
+
return analytics.getFeatureFlag(flagKey);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/types.ts
|
|
172
|
+
import { z } from "zod";
|
|
173
|
+
var eventTypes = [
|
|
174
|
+
"app_setup",
|
|
175
|
+
"protocol_installed",
|
|
176
|
+
"interview_started",
|
|
177
|
+
"interview_completed",
|
|
178
|
+
"data_exported",
|
|
179
|
+
"error"
|
|
180
|
+
];
|
|
181
|
+
var legacyEventTypeMap = {
|
|
182
|
+
AppSetup: "app_setup",
|
|
183
|
+
ProtocolInstalled: "protocol_installed",
|
|
184
|
+
InterviewStarted: "interview_started",
|
|
185
|
+
InterviewCompleted: "interview_completed",
|
|
186
|
+
DataExported: "data_exported",
|
|
187
|
+
Error: "error"
|
|
173
188
|
};
|
|
189
|
+
var EventPropertiesSchema = z.object({
|
|
190
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
191
|
+
});
|
|
192
|
+
var ErrorPropertiesSchema = EventPropertiesSchema.extend({
|
|
193
|
+
message: z.string(),
|
|
194
|
+
name: z.string(),
|
|
195
|
+
stack: z.string().optional(),
|
|
196
|
+
cause: z.string().optional()
|
|
197
|
+
});
|
|
174
198
|
export {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
createRouteHandler,
|
|
199
|
+
AnalyticsProvider,
|
|
200
|
+
defaultConfig,
|
|
201
|
+
ensureError,
|
|
179
202
|
eventTypes,
|
|
180
|
-
|
|
203
|
+
isDisabledByEnv,
|
|
204
|
+
legacyEventTypeMap,
|
|
205
|
+
mergeConfig,
|
|
206
|
+
useAnalytics,
|
|
207
|
+
useFeatureFlag,
|
|
208
|
+
useFeatureFlagValue
|
|
181
209
|
};
|
|
182
210
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/utils.ts"],"sourcesContent":["import { type NextRequest, NextResponse } from \"next/server\";\nimport z from \"zod\";\nimport { ensureError } from \"./utils\";\n\n// Todo: it would be great to work out a way to support arbitrary types here.\nexport const eventTypes = [\n\t\"AppSetup\",\n\t\"ProtocolInstalled\",\n\t\"InterviewStarted\",\n\t\"InterviewCompleted\",\n\t\"DataExported\",\n] as const;\n\nconst EventSchema = z.object({\n\ttype: z.enum(eventTypes),\n});\n\nconst ErrorSchema = z.object({\n\ttype: z.literal(\"Error\"),\n\tmessage: z.string(),\n\tname: z.string(),\n\tstack: z.string().optional(),\n\tcause: z.string().optional(),\n});\n\nconst SharedEventAndErrorSchema = z.object({\n\tmetadata: z.record(z.string(), z.unknown()).optional(),\n});\n\n/**\n * Raw events are the payload that is sent to trackEvent, which can be either\n * general events or errors. We discriminate on the `type` property to determine\n * which schema to use, and then merge the shared properties.\n */\nexport const RawEventSchema = z.discriminatedUnion(\"type\", [\n\tSharedEventAndErrorSchema.merge(EventSchema),\n\tSharedEventAndErrorSchema.merge(ErrorSchema),\n]);\nexport type RawEvent = z.infer<typeof RawEventSchema>;\n\n/**\n * Trackable events are the events that are sent to the route handler by\n * `trackEvent`. The function adds the timestamp to ensure it is not inaccurate\n * due to network latency or processing time.\n */\nconst TrackablePropertiesSchema = z.object({\n\ttimestamp: z.string(),\n});\n\nexport const TrackableEventSchema = z.intersection(RawEventSchema, TrackablePropertiesSchema);\nexport type TrackableEvent = z.infer<typeof TrackableEventSchema>;\n\n/**\n * Dispatchable events are the events that are sent to the platform. The route\n * handler injects the installationId and countryISOCode properties.\n */\nconst DispatchablePropertiesSchema = z.object({\n\tinstallationId: z.string(),\n\tcountryISOCode: z.string(),\n});\n\n/**\n * The final schema for an analytics event. This is the schema that is used to\n * validate the event before it is inserted into the database. It is the\n * intersection of the trackable event and the dispatchable properties.\n */\nexport const AnalyticsEventSchema = z.intersection(TrackableEventSchema, DispatchablePropertiesSchema);\nexport type analyticsEvent = z.infer<typeof AnalyticsEventSchema>;\n\ntype GeoData = {\n\tstatus: \"success\" | \"fail\";\n\tcountryCode: string;\n\tmessage: string;\n};\n\nexport const createRouteHandler = ({\n\tplatformUrl = \"https://analytics.networkcanvas.com\",\n\tinstallationId,\n\tdisableAnalytics,\n}: {\n\tplatformUrl?: string;\n\tinstallationId: string;\n\tdisableAnalytics?: boolean;\n}) => {\n\treturn async (request: NextRequest) => {\n\t\ttry {\n\t\t\tconst incomingEvent = (await request.json()) as unknown;\n\n\t\t\t// Check if analytics is disabled\n\t\t\tif (disableAnalytics) {\n\t\t\t\tconsole.info(\"🛑 Analytics disabled. Payload not sent.\");\n\t\t\t\ttry {\n\t\t\t\t\tconsole.info(\"Payload:\", \"\\n\", JSON.stringify(incomingEvent, null, 2));\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconsole.error(\"Error stringifying payload:\", e);\n\t\t\t\t}\n\n\t\t\t\treturn NextResponse.json({ message: \"Analytics disabled\" }, { status: 200 });\n\t\t\t}\n\n\t\t\t// Validate the event\n\t\t\tconst trackableEvent = TrackableEventSchema.safeParse(incomingEvent);\n\n\t\t\tif (!trackableEvent.success) {\n\t\t\t\tconsole.error(\"Invalid event:\", trackableEvent.error);\n\t\t\t\treturn NextResponse.json({ error: \"Invalid event\" }, { status: 400 });\n\t\t\t}\n\n\t\t\t// We don't want failures in third party services to prevent us from\n\t\t\t// tracking analytics events, so we'll catch any errors and log them\n\t\t\t// and continue with an 'Unknown' country code.\n\t\t\tlet countryISOCode = \"Unknown\";\n\t\t\ttry {\n\t\t\t\tconst ip = await fetch(\"https://api64.ipify.org\").then((res) => res.text());\n\n\t\t\t\tif (!ip) {\n\t\t\t\t\tthrow new Error(\"Could not fetch IP address\");\n\t\t\t\t}\n\n\t\t\t\tconst geoData = (await fetch(`http://ip-api.com/json/${ip}`).then((res) => res.json())) as GeoData;\n\n\t\t\t\tif (geoData.status === \"success\") {\n\t\t\t\t\tcountryISOCode = geoData.countryCode;\n\t\t\t\t} else {\n\t\t\t\t\tthrow new Error(geoData.message);\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error(\"Geolocation failed:\", e);\n\t\t\t}\n\n\t\t\tconst analyticsEvent: analyticsEvent = {\n\t\t\t\t...trackableEvent.data,\n\t\t\t\tinstallationId,\n\t\t\t\tcountryISOCode,\n\t\t\t};\n\n\t\t\t// Forward to backend\n\t\t\tconst response = await fetch(`${platformUrl}/api/event`, {\n\t\t\t\tkeepalive: true,\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(analyticsEvent),\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\tlet error = `Analytics platform returned an unexpected error: ${response.statusText}`;\n\n\t\t\t\tif (response.status === 400) {\n\t\t\t\t\terror = \"Analytics platform rejected the event as invalid. Please check the event schema\";\n\t\t\t\t}\n\n\t\t\t\tif (response.status === 404) {\n\t\t\t\t\terror =\n\t\t\t\t\t\t\"Analytics platform could not be reached. Please specify a valid platform URL, or check that the platform is online.\";\n\t\t\t\t}\n\n\t\t\t\tif (response.status === 500) {\n\t\t\t\t\terror = \"Analytics platform returned an internal server error. Please check the platform logs.\";\n\t\t\t\t}\n\n\t\t\t\tconsole.info(`⚠️ Analytics platform rejected event: ${error}`);\n\t\t\t\treturn Response.json(\n\t\t\t\t\t{\n\t\t\t\t\t\terror,\n\t\t\t\t\t},\n\t\t\t\t\t{ status: 500 },\n\t\t\t\t);\n\t\t\t}\n\t\t\tconsole.info(\"🚀 Analytics event sent to platform!\");\n\t\t\treturn Response.json({ message: \"Event forwarded successfully\" });\n\t\t} catch (e) {\n\t\t\tconst error = ensureError(e);\n\t\t\tconsole.info(\"🚫 Internal error with sending analytics event.\");\n\n\t\t\treturn Response.json({ error: `Error in analytics route handler: ${error.message}` }, { status: 500 });\n\t\t}\n\t};\n};\n\nexport const makeEventTracker =\n\t(options?: { endpoint?: string }) =>\n\tasync (\n\t\tevent: RawEvent,\n\t): Promise<{\n\t\terror: string | null;\n\t\tsuccess: boolean;\n\t}> => {\n\t\t// We use a relative path by default, which should automatically use the\n\t\t// same origin as the page that is sending the event.\n\t\tconst endpoint = options?.endpoint ?? \"/api/analytics\";\n\n\t\tconst eventWithTimeStamp: TrackableEvent = {\n\t\t\t...event,\n\t\t\ttimestamp: new Date().toJSON(),\n\t\t};\n\n\t\ttry {\n\t\t\tconst response = await fetch(endpoint, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tkeepalive: true,\n\t\t\t\tbody: JSON.stringify(eventWithTimeStamp),\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\tif (response.status === 404) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\terror: \"Analytics endpoint not found, did you forget to add the route?\",\n\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\t// createRouteHandler will return a 400 if the event failed schema validation.\n\t\t\t\tif (response.status === 400) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\terror: `Invalid event sent to analytics endpoint: ${response.statusText}`,\n\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\t// createRouteHandler will return a 500 for all error states\n\t\t\t\treturn {\n\t\t\t\t\terror: `Internal server error when sending analytics event: ${response.statusText}. Check the route handler implementation.`,\n\t\t\t\t\tsuccess: false,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn { error: null, success: true };\n\t\t} catch (e) {\n\t\t\tconst error = ensureError(e);\n\t\t\treturn {\n\t\t\t\terror: `Internal error when sending analytics event: ${error.message}`,\n\t\t\t\tsuccess: false,\n\t\t\t};\n\t\t}\n\t};\n","// Helper function that ensures that a value is an Error\nexport function ensureError(value: unknown): Error {\n\tif (!value) return new Error(\"No value was thrown\");\n\n\tif (value instanceof Error) return value;\n\n\t// Test if value inherits from Error\n\tif (Object.prototype.isPrototypeOf.call(value, Error)) return value as Error & typeof value;\n\n\tlet stringified = \"[Unable to stringify the thrown value]\";\n\ttry {\n\t\tstringified = JSON.stringify(value);\n\t} catch (e) {\n\t\tconsole.error(e);\n\t}\n\n\tconst error = new Error(`This value was thrown as is, not through an Error: ${stringified}`);\n\treturn error;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/hooks.ts","../src/provider.tsx","../src/client.ts","../src/types.ts"],"sourcesContent":["\"use client\";\n\nimport { useContext } from \"react\";\nimport { AnalyticsContext } from \"./provider\";\nimport type { Analytics } from \"./types\";\n\n/**\n * Hook to access analytics functionality in React components\n *\n * @example\n * ```tsx\n * import { useAnalytics } from '@codaco/analytics';\n *\n * function MyComponent() {\n * const { trackEvent, trackError } = useAnalytics();\n *\n * const handleAction = () => {\n * trackEvent('protocol_installed', {\n * metadata: { protocolName: 'My Protocol' }\n * });\n * };\n *\n * return <button onClick={handleAction}>Install Protocol</button>;\n * }\n * ```\n */\nexport function useAnalytics(): Analytics {\n\tconst analytics = useContext(AnalyticsContext);\n\n\tif (!analytics) {\n\t\tthrow new Error(\"useAnalytics must be used within an AnalyticsProvider\");\n\t}\n\n\treturn analytics;\n}\n\n/**\n * Hook to access feature flags\n *\n * @example\n * ```tsx\n * import { useFeatureFlag } from '@codaco/analytics';\n *\n * function MyComponent() {\n * const isNewFeatureEnabled = useFeatureFlag('new-feature');\n *\n * if (isNewFeatureEnabled) {\n * return <NewFeature />;\n * }\n *\n * return <OldFeature />;\n * }\n * ```\n */\nexport function useFeatureFlag(flagKey: string): boolean {\n\tconst analytics = useAnalytics();\n\treturn analytics.isFeatureEnabled(flagKey) ?? false;\n}\n\n/**\n * Hook to access feature flag values (for multivariate flags)\n *\n * @example\n * ```tsx\n * import { useFeatureFlagValue } from '@codaco/analytics';\n *\n * function MyComponent() {\n * const theme = useFeatureFlagValue('theme-variant');\n *\n * return <div className={theme === 'dark' ? 'dark-theme' : 'light-theme'}>\n * Content\n * </div>;\n * }\n * ```\n */\nexport function useFeatureFlagValue(flagKey: string): string | boolean | undefined {\n\tconst analytics = useAnalytics();\n\treturn analytics.getFeatureFlag(flagKey);\n}\n","\"use client\";\n\nimport { createContext, type ReactNode, useEffect, useRef } from \"react\";\nimport { createAnalytics } from \"./client\";\nimport { mergeConfig } from \"./config\";\nimport type { Analytics, AnalyticsConfig } from \"./types\";\n\n/**\n * React Context for analytics\n */\nexport const AnalyticsContext = createContext<Analytics | null>(null);\n\n/**\n * Props for the AnalyticsProvider\n */\nexport type AnalyticsProviderProps = {\n\tchildren: ReactNode;\n\tconfig: AnalyticsConfig;\n};\n\n/**\n * Provider component that initializes PostHog and provides analytics context\n *\n * @example\n * ```tsx\n * import { AnalyticsProvider } from '@codaco/analytics';\n *\n * function App({ children }) {\n * return (\n * <AnalyticsProvider\n * config={{\n * installationId: 'your-installation-id',\n * apiKey: 'phc_your_api_key', // optional if set via env\n * apiHost: 'https://ph-relay.networkcanvas.com', // optional\n * }}\n * >\n * {children}\n * </AnalyticsProvider>\n * );\n * }\n * ```\n */\nexport function AnalyticsProvider({ children, config }: AnalyticsProviderProps) {\n\tconst analyticsRef = useRef<Analytics | null>(null);\n\n\t// Initialize analytics only once\n\tuseEffect(() => {\n\t\tif (!analyticsRef.current) {\n\t\t\tconst mergedConfig = mergeConfig(config);\n\t\t\tanalyticsRef.current = createAnalytics(mergedConfig);\n\t\t}\n\t}, []); // Empty deps - only initialize once\n\n\t// Don't render children until analytics is initialized\n\tif (!analyticsRef.current) {\n\t\treturn null;\n\t}\n\n\treturn <AnalyticsContext.Provider value={analyticsRef.current}>{children}</AnalyticsContext.Provider>;\n}\n","import posthog from \"posthog-js\";\nimport type { Analytics, AnalyticsConfig, ErrorProperties, EventProperties, EventType } from \"./types\";\nimport { ensureError } from \"./utils\";\n\n/**\n * Create a client-side analytics instance\n * This wraps PostHog with Network Canvas-specific functionality\n */\nexport function createAnalytics(config: Required<AnalyticsConfig>): Analytics {\n\tconst { apiHost, apiKey, installationId, disabled, debug, posthogOptions } = config;\n\n\t// If analytics is disabled, return a no-op implementation\n\tif (disabled) {\n\t\treturn createNoOpAnalytics(installationId);\n\t}\n\n\t// Initialize PostHog\n\tposthog.init(apiKey, {\n\t\tapi_host: apiHost,\n\t\tloaded: (posthogInstance) => {\n\t\t\t// Set installation ID as a super property (included with every event)\n\t\t\tposthogInstance.register({\n\t\t\t\tinstallation_id: installationId,\n\t\t\t});\n\n\t\t\tif (debug) {\n\t\t\t\tposthogInstance.debug();\n\t\t\t}\n\t\t},\n\t\t...posthogOptions,\n\t});\n\n\treturn {\n\t\ttrackEvent: (eventType: EventType | string, properties?: EventProperties) => {\n\t\t\tif (disabled) return;\n\n\t\t\ttry {\n\t\t\t\tposthog.capture(eventType, {\n\t\t\t\t\t...properties,\n\t\t\t\t\t// Flatten metadata into properties for better PostHog integration\n\t\t\t\t\t...(properties?.metadata ?? {}),\n\t\t\t\t});\n\t\t\t} catch (_e) {\n\t\t\t\tif (debug) {\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\ttrackError: (error: Error, additionalProperties?: EventProperties) => {\n\t\t\tif (disabled) return;\n\n\t\t\ttry {\n\t\t\t\tconst errorObj = ensureError(error);\n\t\t\t\tconst errorProperties: ErrorProperties = {\n\t\t\t\t\tmessage: errorObj.message,\n\t\t\t\t\tname: errorObj.name,\n\t\t\t\t\tstack: errorObj.stack,\n\t\t\t\t\tcause: errorObj.cause ? String(errorObj.cause) : undefined,\n\t\t\t\t\t...additionalProperties,\n\t\t\t\t};\n\n\t\t\t\tposthog.capture(\"error\", {\n\t\t\t\t\t...errorProperties,\n\t\t\t\t\t// Flatten metadata\n\t\t\t\t\t...(additionalProperties?.metadata ?? {}),\n\t\t\t\t});\n\t\t\t} catch (_e) {\n\t\t\t\tif (debug) {\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tisFeatureEnabled: (flagKey: string) => {\n\t\t\tif (disabled) return false;\n\n\t\t\ttry {\n\t\t\t\treturn posthog.isFeatureEnabled(flagKey);\n\t\t\t} catch (_e) {\n\t\t\t\tif (debug) {\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t},\n\n\t\tgetFeatureFlag: (flagKey: string) => {\n\t\t\tif (disabled) return undefined;\n\n\t\t\ttry {\n\t\t\t\treturn posthog.getFeatureFlag(flagKey);\n\t\t\t} catch (_e) {\n\t\t\t\tif (debug) {\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t},\n\n\t\treloadFeatureFlags: () => {\n\t\t\tif (disabled) return;\n\n\t\t\ttry {\n\t\t\t\tposthog.reloadFeatureFlags();\n\t\t\t} catch (_e) {\n\t\t\t\tif (debug) {\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tidentify: (distinctId: string, properties?: Record<string, unknown>) => {\n\t\t\tif (disabled) return;\n\n\t\t\ttry {\n\t\t\t\tposthog.identify(distinctId, properties);\n\t\t\t} catch (_e) {\n\t\t\t\tif (debug) {\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\treset: () => {\n\t\t\tif (disabled) return;\n\n\t\t\ttry {\n\t\t\t\tposthog.reset();\n\t\t\t} catch (_e) {\n\t\t\t\tif (debug) {\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tisEnabled: () => !disabled,\n\n\t\tgetInstallationId: () => installationId,\n\t};\n}\n\n/**\n * Create a no-op analytics instance when analytics is disabled\n */\nfunction createNoOpAnalytics(installationId: string): Analytics {\n\treturn {\n\t\ttrackEvent: () => {},\n\t\ttrackError: () => {},\n\t\tisFeatureEnabled: () => false,\n\t\tgetFeatureFlag: () => undefined,\n\t\treloadFeatureFlags: () => {},\n\t\tidentify: () => {},\n\t\treset: () => {},\n\t\tisEnabled: () => false,\n\t\tgetInstallationId: () => installationId,\n\t};\n}\n","import { z } from \"zod\";\n\n/**\n * Event types supported by the analytics system.\n * These are converted to snake_case for PostHog.\n */\nexport const eventTypes = [\n\t\"app_setup\",\n\t\"protocol_installed\",\n\t\"interview_started\",\n\t\"interview_completed\",\n\t\"data_exported\",\n\t\"error\",\n] as const;\n\nexport type EventType = (typeof eventTypes)[number];\n\n/**\n * Legacy event type mapping for backward compatibility\n */\nexport const legacyEventTypeMap: Record<string, EventType> = {\n\tAppSetup: \"app_setup\",\n\tProtocolInstalled: \"protocol_installed\",\n\tInterviewStarted: \"interview_started\",\n\tInterviewCompleted: \"interview_completed\",\n\tDataExported: \"data_exported\",\n\tError: \"error\",\n};\n\n/**\n * Standard event properties that can be sent with any event\n */\nexport const EventPropertiesSchema = z.object({\n\tmetadata: z.record(z.string(), z.unknown()).optional(),\n});\n\nexport type EventProperties = z.infer<typeof EventPropertiesSchema>;\n\n/**\n * Error-specific properties for error tracking\n */\nexport const ErrorPropertiesSchema = EventPropertiesSchema.extend({\n\tmessage: z.string(),\n\tname: z.string(),\n\tstack: z.string().optional(),\n\tcause: z.string().optional(),\n});\n\nexport type ErrorProperties = z.infer<typeof ErrorPropertiesSchema>;\n\n/**\n * Analytics configuration options\n *\n * This package is designed to work exclusively with the Cloudflare Worker\n * reverse proxy at ph-relay.networkcanvas.com. All authentication is handled\n * by the worker, so the API key is optional.\n */\nexport type AnalyticsConfig = {\n\t/**\n\t * PostHog API host - should point to the Cloudflare Worker reverse proxy\n\t * Defaults to \"https://ph-relay.networkcanvas.com\"\n\t */\n\tapiHost?: string;\n\n\t/**\n\t * PostHog project API key (optional)\n\t *\n\t * When using the reverse proxy (default), authentication is handled by the\n\t * Cloudflare Worker. A placeholder key will be used for client-side PostHog\n\t * initialization if not provided.\n\t *\n\t * Only set this if you need to override the default behavior.\n\t */\n\tapiKey?: string;\n\n\t/**\n\t * Unique identifier for this installation/deployment\n\t * This is included with every event as a super property\n\t */\n\tinstallationId: string;\n\n\t/**\n\t * Disable all analytics tracking\n\t * Can be set via DISABLE_ANALYTICS or NEXT_PUBLIC_DISABLE_ANALYTICS env var\n\t */\n\tdisabled?: boolean;\n\n\t/**\n\t * Enable debug mode for PostHog\n\t */\n\tdebug?: boolean;\n\n\t/**\n\t * Additional options to pass to PostHog initialization\n\t */\n\tposthogOptions?: {\n\t\t/**\n\t\t * Disable session recording\n\t\t */\n\t\tdisable_session_recording?: boolean;\n\n\t\t/**\n\t\t * Autocapture settings\n\t\t */\n\t\tautocapture?: boolean;\n\n\t\t/**\n\t\t * Capture pageviews automatically\n\t\t */\n\t\tcapture_pageview?: boolean;\n\n\t\t/**\n\t\t * Capture pageleave events\n\t\t */\n\t\tcapture_pageleave?: boolean;\n\n\t\t/**\n\t\t * Cross-subdomain cookie\n\t\t */\n\t\tcross_subdomain_cookie?: boolean;\n\n\t\t/**\n\t\t * Advanced feature flags support\n\t\t */\n\t\tadvanced_disable_feature_flags?: boolean;\n\n\t\t/**\n\t\t * Other PostHog options\n\t\t */\n\t\t[key: string]: unknown;\n\t};\n};\n\n/**\n * Analytics instance interface\n */\nexport type Analytics = {\n\t/**\n\t * Track a custom event\n\t */\n\ttrackEvent: (eventType: EventType | string, properties?: EventProperties) => void;\n\n\t/**\n\t * Track an error with full stack trace\n\t */\n\ttrackError: (error: Error, additionalProperties?: EventProperties) => void;\n\n\t/**\n\t * Check if a feature flag is enabled\n\t */\n\tisFeatureEnabled: (flagKey: string) => boolean | undefined;\n\n\t/**\n\t * Get the value of a feature flag\n\t */\n\tgetFeatureFlag: (flagKey: string) => string | boolean | undefined;\n\n\t/**\n\t * Reload feature flags from PostHog\n\t */\n\treloadFeatureFlags: () => void;\n\n\t/**\n\t * Identify a user (optional - for advanced use cases)\n\t * Note: By default we only track installations, not users\n\t */\n\tidentify: (distinctId: string, properties?: Record<string, unknown>) => void;\n\n\t/**\n\t * Reset the user identity\n\t */\n\treset: () => void;\n\n\t/**\n\t * Check if analytics is enabled\n\t */\n\tisEnabled: () => boolean;\n\n\t/**\n\t * Get the installation ID\n\t */\n\tgetInstallationId: () => string;\n};\n"],"mappings":"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","names":[]}
|