@codaco/analytics 7.0.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -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 -144
- package/dist/index.js +188 -183
- 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-Ymgjicqi.d.ts +145 -0
- package/package.json +31 -24
- 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 -264
- package/src/provider.tsx +60 -0
- package/src/server.ts +213 -0
- package/src/types.ts +183 -0
- package/src/utils.ts +13 -16
- package/tsconfig.json +7 -7
- package/vitest.config.ts +18 -0
- package/.turbo/turbo-build.log +0 -16
- package/.turbo/turbo-lint.log +0 -7
package/src/client.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import posthog from "posthog-js";
|
|
2
|
+
import type { Analytics, AnalyticsConfig, ErrorProperties, EventProperties, EventType } from "./types";
|
|
3
|
+
import { ensureError } from "./utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a client-side analytics instance
|
|
7
|
+
* This wraps PostHog with Network Canvas-specific functionality
|
|
8
|
+
*/
|
|
9
|
+
export function createAnalytics(config: Required<AnalyticsConfig>): Analytics {
|
|
10
|
+
const { apiHost, apiKey, installationId, disabled, debug, posthogOptions } = config;
|
|
11
|
+
|
|
12
|
+
// If analytics is disabled, return a no-op implementation
|
|
13
|
+
if (disabled) {
|
|
14
|
+
return createNoOpAnalytics(installationId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Initialize PostHog
|
|
18
|
+
posthog.init(apiKey, {
|
|
19
|
+
api_host: apiHost,
|
|
20
|
+
loaded: (posthogInstance) => {
|
|
21
|
+
// Set installation ID as a super property (included with every event)
|
|
22
|
+
posthogInstance.register({
|
|
23
|
+
installation_id: installationId,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (debug) {
|
|
27
|
+
posthogInstance.debug();
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
...posthogOptions,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
trackEvent: (eventType: EventType | string, properties?: EventProperties) => {
|
|
35
|
+
if (disabled) return;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
posthog.capture(eventType, {
|
|
39
|
+
...properties,
|
|
40
|
+
// Flatten metadata into properties for better PostHog integration
|
|
41
|
+
...(properties?.metadata ?? {}),
|
|
42
|
+
});
|
|
43
|
+
} catch (_e) {
|
|
44
|
+
if (debug) {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
trackError: (error: Error, additionalProperties?: EventProperties) => {
|
|
50
|
+
if (disabled) return;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const errorObj = ensureError(error);
|
|
54
|
+
const errorProperties: ErrorProperties = {
|
|
55
|
+
message: errorObj.message,
|
|
56
|
+
name: errorObj.name,
|
|
57
|
+
stack: errorObj.stack,
|
|
58
|
+
cause: errorObj.cause ? String(errorObj.cause) : undefined,
|
|
59
|
+
...additionalProperties,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
posthog.capture("error", {
|
|
63
|
+
...errorProperties,
|
|
64
|
+
// Flatten metadata
|
|
65
|
+
...(additionalProperties?.metadata ?? {}),
|
|
66
|
+
});
|
|
67
|
+
} catch (_e) {
|
|
68
|
+
if (debug) {
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
isFeatureEnabled: (flagKey: string) => {
|
|
74
|
+
if (disabled) return false;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
return posthog.isFeatureEnabled(flagKey);
|
|
78
|
+
} catch (_e) {
|
|
79
|
+
if (debug) {
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
getFeatureFlag: (flagKey: string) => {
|
|
86
|
+
if (disabled) return undefined;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
return posthog.getFeatureFlag(flagKey);
|
|
90
|
+
} catch (_e) {
|
|
91
|
+
if (debug) {
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
reloadFeatureFlags: () => {
|
|
98
|
+
if (disabled) return;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
posthog.reloadFeatureFlags();
|
|
102
|
+
} catch (_e) {
|
|
103
|
+
if (debug) {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
identify: (distinctId: string, properties?: Record<string, unknown>) => {
|
|
109
|
+
if (disabled) return;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
posthog.identify(distinctId, properties);
|
|
113
|
+
} catch (_e) {
|
|
114
|
+
if (debug) {
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
reset: () => {
|
|
120
|
+
if (disabled) return;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
posthog.reset();
|
|
124
|
+
} catch (_e) {
|
|
125
|
+
if (debug) {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
isEnabled: () => !disabled,
|
|
131
|
+
|
|
132
|
+
getInstallationId: () => installationId,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create a no-op analytics instance when analytics is disabled
|
|
138
|
+
*/
|
|
139
|
+
function createNoOpAnalytics(installationId: string): Analytics {
|
|
140
|
+
return {
|
|
141
|
+
trackEvent: () => {},
|
|
142
|
+
trackError: () => {},
|
|
143
|
+
isFeatureEnabled: () => false,
|
|
144
|
+
getFeatureFlag: () => undefined,
|
|
145
|
+
reloadFeatureFlags: () => {},
|
|
146
|
+
identify: () => {},
|
|
147
|
+
reset: () => {},
|
|
148
|
+
isEnabled: () => false,
|
|
149
|
+
getInstallationId: () => installationId,
|
|
150
|
+
};
|
|
151
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { AnalyticsConfig } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hardcoded PostHog API host - always uses the Cloudflare Worker reverse proxy
|
|
5
|
+
* Authentication is handled by the worker at this endpoint
|
|
6
|
+
*/
|
|
7
|
+
const POSTHOG_PROXY_HOST = "https://ph-relay.networkcanvas.com";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Dummy API key used for proxy mode
|
|
11
|
+
* PostHog JS library requires an API key for initialization, but when using
|
|
12
|
+
* the reverse proxy, authentication is handled by the Cloudflare Worker.
|
|
13
|
+
* This placeholder key is used for client-side initialization only.
|
|
14
|
+
*/
|
|
15
|
+
const PROXY_MODE_DUMMY_KEY = "phc_proxy_mode_placeholder";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if analytics is disabled via environment variables
|
|
19
|
+
*/
|
|
20
|
+
export function isDisabledByEnv(): boolean {
|
|
21
|
+
if (typeof process === "undefined") {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return process.env.DISABLE_ANALYTICS === "true" || process.env.NEXT_PUBLIC_DISABLE_ANALYTICS === "true";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Default configuration for analytics
|
|
30
|
+
* API host and key are hardcoded, but disabled flag can be set via environment variables
|
|
31
|
+
*/
|
|
32
|
+
export const defaultConfig: Partial<AnalyticsConfig> = {
|
|
33
|
+
// Always use the Cloudflare Worker reverse proxy
|
|
34
|
+
apiHost: POSTHOG_PROXY_HOST,
|
|
35
|
+
|
|
36
|
+
// Analytics enabled by default (can be disabled via env var or config option)
|
|
37
|
+
disabled: false,
|
|
38
|
+
|
|
39
|
+
// Debug mode disabled by default
|
|
40
|
+
debug: false,
|
|
41
|
+
|
|
42
|
+
// Default PostHog options
|
|
43
|
+
posthogOptions: {
|
|
44
|
+
// Disable session recording by default (can be enabled per-app)
|
|
45
|
+
disable_session_recording: true,
|
|
46
|
+
|
|
47
|
+
// Disable autocapture to keep events clean and intentional
|
|
48
|
+
autocapture: false,
|
|
49
|
+
|
|
50
|
+
// Disable automatic pageview capture (apps can enable if needed)
|
|
51
|
+
capture_pageview: false,
|
|
52
|
+
|
|
53
|
+
// Disable pageleave events
|
|
54
|
+
capture_pageleave: false,
|
|
55
|
+
|
|
56
|
+
// Don't use cross-subdomain cookies
|
|
57
|
+
cross_subdomain_cookie: false,
|
|
58
|
+
|
|
59
|
+
// Enable feature flags by default
|
|
60
|
+
advanced_disable_feature_flags: false,
|
|
61
|
+
|
|
62
|
+
// Send feature flag events
|
|
63
|
+
advanced_disable_feature_flags_on_first_load: false,
|
|
64
|
+
|
|
65
|
+
// Enable persistence for feature flags
|
|
66
|
+
persistence: "localStorage+cookie",
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Merge user config with defaults
|
|
72
|
+
*
|
|
73
|
+
* Note: This package is designed to work exclusively with the Cloudflare Worker
|
|
74
|
+
* reverse proxy (ph-relay.networkcanvas.com). Authentication is handled by the
|
|
75
|
+
* worker, so the API key is optional and defaults to a placeholder value.
|
|
76
|
+
*
|
|
77
|
+
* The only environment variable checked is DISABLE_ANALYTICS / NEXT_PUBLIC_DISABLE_ANALYTICS
|
|
78
|
+
* for disabling tracking. All other configuration is hardcoded or passed explicitly.
|
|
79
|
+
*/
|
|
80
|
+
export function mergeConfig(userConfig: AnalyticsConfig): Required<AnalyticsConfig> {
|
|
81
|
+
return {
|
|
82
|
+
apiHost: userConfig.apiHost ?? defaultConfig.apiHost ?? POSTHOG_PROXY_HOST,
|
|
83
|
+
apiKey: userConfig.apiKey ?? PROXY_MODE_DUMMY_KEY,
|
|
84
|
+
installationId: userConfig.installationId,
|
|
85
|
+
disabled: userConfig.disabled ?? isDisabledByEnv() ?? defaultConfig.disabled ?? false,
|
|
86
|
+
debug: userConfig.debug ?? defaultConfig.debug ?? false,
|
|
87
|
+
posthogOptions: {
|
|
88
|
+
...defaultConfig.posthogOptions,
|
|
89
|
+
...userConfig.posthogOptions,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useContext } from "react";
|
|
4
|
+
import { AnalyticsContext } from "./provider";
|
|
5
|
+
import type { Analytics } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook to access analytics functionality in React components
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import { useAnalytics } from '@codaco/analytics';
|
|
13
|
+
*
|
|
14
|
+
* function MyComponent() {
|
|
15
|
+
* const { trackEvent, trackError } = useAnalytics();
|
|
16
|
+
*
|
|
17
|
+
* const handleAction = () => {
|
|
18
|
+
* trackEvent('protocol_installed', {
|
|
19
|
+
* metadata: { protocolName: 'My Protocol' }
|
|
20
|
+
* });
|
|
21
|
+
* };
|
|
22
|
+
*
|
|
23
|
+
* return <button onClick={handleAction}>Install Protocol</button>;
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function useAnalytics(): Analytics {
|
|
28
|
+
const analytics = useContext(AnalyticsContext);
|
|
29
|
+
|
|
30
|
+
if (!analytics) {
|
|
31
|
+
throw new Error("useAnalytics must be used within an AnalyticsProvider");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return analytics;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Hook to access feature flags
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* import { useFeatureFlag } from '@codaco/analytics';
|
|
43
|
+
*
|
|
44
|
+
* function MyComponent() {
|
|
45
|
+
* const isNewFeatureEnabled = useFeatureFlag('new-feature');
|
|
46
|
+
*
|
|
47
|
+
* if (isNewFeatureEnabled) {
|
|
48
|
+
* return <NewFeature />;
|
|
49
|
+
* }
|
|
50
|
+
*
|
|
51
|
+
* return <OldFeature />;
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function useFeatureFlag(flagKey: string): boolean {
|
|
56
|
+
const analytics = useAnalytics();
|
|
57
|
+
return analytics.isFeatureEnabled(flagKey) ?? false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Hook to access feature flag values (for multivariate flags)
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```tsx
|
|
65
|
+
* import { useFeatureFlagValue } from '@codaco/analytics';
|
|
66
|
+
*
|
|
67
|
+
* function MyComponent() {
|
|
68
|
+
* const theme = useFeatureFlagValue('theme-variant');
|
|
69
|
+
*
|
|
70
|
+
* return <div className={theme === 'dark' ? 'dark-theme' : 'light-theme'}>
|
|
71
|
+
* Content
|
|
72
|
+
* </div>;
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function useFeatureFlagValue(flagKey: string): string | boolean | undefined {
|
|
77
|
+
const analytics = useAnalytics();
|
|
78
|
+
return analytics.getFeatureFlag(flagKey);
|
|
79
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,267 +1,72 @@
|
|
|
1
|
-
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
-
import { ensureError } from './utils';
|
|
3
|
-
import z from 'zod';
|
|
4
|
-
|
|
5
|
-
// Todo: it would be great to work out a way to support arbitrary types here.
|
|
6
|
-
export const eventTypes = [
|
|
7
|
-
'AppSetup',
|
|
8
|
-
'ProtocolInstalled',
|
|
9
|
-
'InterviewStarted',
|
|
10
|
-
'InterviewCompleted',
|
|
11
|
-
'DataExported',
|
|
12
|
-
] as const;
|
|
13
|
-
|
|
14
|
-
const EventSchema = z.object({
|
|
15
|
-
type: z.enum(eventTypes),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const ErrorSchema = z.object({
|
|
19
|
-
type: z.literal('Error'),
|
|
20
|
-
message: z.string(),
|
|
21
|
-
name: z.string(),
|
|
22
|
-
stack: z.string().optional(),
|
|
23
|
-
cause: z.string().optional(),
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const SharedEventAndErrorSchema = z.object({
|
|
27
|
-
metadata: z.record(z.unknown()).optional(),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Raw events are the payload that is sent to trackEvent, which can be either
|
|
32
|
-
* general events or errors. We discriminate on the `type` property to determine
|
|
33
|
-
* which schema to use, and then merge the shared properties.
|
|
34
|
-
*/
|
|
35
|
-
export const RawEventSchema = z.discriminatedUnion('type', [
|
|
36
|
-
SharedEventAndErrorSchema.merge(EventSchema),
|
|
37
|
-
SharedEventAndErrorSchema.merge(ErrorSchema),
|
|
38
|
-
]);
|
|
39
|
-
export type RawEvent = z.infer<typeof RawEventSchema>;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Trackable events are the events that are sent to the route handler by
|
|
43
|
-
* `trackEvent`. The function adds the timestamp to ensure it is not inaccurate
|
|
44
|
-
* due to network latency or processing time.
|
|
45
|
-
*/
|
|
46
|
-
const TrackablePropertiesSchema = z.object({
|
|
47
|
-
timestamp: z.string(),
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
export const TrackableEventSchema = z.intersection(
|
|
51
|
-
RawEventSchema,
|
|
52
|
-
TrackablePropertiesSchema,
|
|
53
|
-
);
|
|
54
|
-
export type TrackableEvent = z.infer<typeof TrackableEventSchema>;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Dispatchable events are the events that are sent to the platform. The route
|
|
58
|
-
* handler injects the installationId and countryISOCode properties.
|
|
59
|
-
*/
|
|
60
|
-
const DispatchablePropertiesSchema = z.object({
|
|
61
|
-
installationId: z.string(),
|
|
62
|
-
countryISOCode: z.string(),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
1
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
2
|
+
* @codaco/analytics
|
|
3
|
+
*
|
|
4
|
+
* PostHog analytics wrapper for Network Canvas applications
|
|
5
|
+
* with installation ID tracking, error reporting, and feature flags.
|
|
6
|
+
*
|
|
7
|
+
* @example Client-side usage (React):
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { AnalyticsProvider, useAnalytics } from '@codaco/analytics';
|
|
10
|
+
*
|
|
11
|
+
* // In your root layout
|
|
12
|
+
* export default function RootLayout({ children }) {
|
|
13
|
+
* return (
|
|
14
|
+
* <AnalyticsProvider
|
|
15
|
+
* config={{
|
|
16
|
+
* installationId: 'your-unique-installation-id',
|
|
17
|
+
* }}
|
|
18
|
+
* >
|
|
19
|
+
* {children}
|
|
20
|
+
* </AnalyticsProvider>
|
|
21
|
+
* );
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* // In your components
|
|
25
|
+
* function MyComponent() {
|
|
26
|
+
* const { trackEvent, trackError } = useAnalytics();
|
|
27
|
+
*
|
|
28
|
+
* const handleAction = () => {
|
|
29
|
+
* trackEvent('app_setup', {
|
|
30
|
+
* metadata: { version: '1.0.0' }
|
|
31
|
+
* });
|
|
32
|
+
* };
|
|
33
|
+
*
|
|
34
|
+
* return <button onClick={handleAction}>Setup</button>;
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @example Server-side usage (API routes, server actions):
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { serverAnalytics } from '@codaco/analytics/server';
|
|
41
|
+
*
|
|
42
|
+
* export async function POST(request: Request) {
|
|
43
|
+
* serverAnalytics.trackEvent('data_exported', {
|
|
44
|
+
* metadata: { format: 'csv' }
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* // ... rest of your handler
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
69
50
|
*/
|
|
70
|
-
export const AnalyticsEventSchema = z.intersection(
|
|
71
|
-
TrackableEventSchema,
|
|
72
|
-
DispatchablePropertiesSchema,
|
|
73
|
-
);
|
|
74
|
-
export type analyticsEvent = z.infer<typeof AnalyticsEventSchema>;
|
|
75
|
-
|
|
76
|
-
type GeoData = {
|
|
77
|
-
status: 'success' | 'fail';
|
|
78
|
-
countryCode: string;
|
|
79
|
-
message: string;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
export const createRouteHandler = ({
|
|
83
|
-
platformUrl = 'https://analytics.networkcanvas.com',
|
|
84
|
-
installationId,
|
|
85
|
-
disableAnalytics,
|
|
86
|
-
}: {
|
|
87
|
-
platformUrl?: string;
|
|
88
|
-
installationId: string;
|
|
89
|
-
disableAnalytics?: boolean;
|
|
90
|
-
}) => {
|
|
91
|
-
return async (request: NextRequest) => {
|
|
92
|
-
try {
|
|
93
|
-
const incomingEvent = (await request.json()) as unknown;
|
|
94
|
-
|
|
95
|
-
// Check if analytics is disabled
|
|
96
|
-
if (disableAnalytics) {
|
|
97
|
-
// eslint-disable-next-line no-console
|
|
98
|
-
console.info('🛑 Analytics disabled. Payload not sent.');
|
|
99
|
-
try {
|
|
100
|
-
// eslint-disable-next-line no-console
|
|
101
|
-
console.info(
|
|
102
|
-
'Payload:',
|
|
103
|
-
'\n',
|
|
104
|
-
JSON.stringify(incomingEvent, null, 2),
|
|
105
|
-
);
|
|
106
|
-
} catch (e) {
|
|
107
|
-
// eslint-disable-next-line no-console
|
|
108
|
-
console.error('Error stringifying payload:', e);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return NextResponse.json(
|
|
112
|
-
{ message: 'Analytics disabled' },
|
|
113
|
-
{ status: 200 },
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Validate the event
|
|
118
|
-
const trackableEvent = TrackableEventSchema.safeParse(incomingEvent);
|
|
119
|
-
|
|
120
|
-
if (!trackableEvent.success) {
|
|
121
|
-
// eslint-disable-next-line no-console
|
|
122
|
-
console.error('Invalid event:', trackableEvent.error);
|
|
123
|
-
return NextResponse.json({ error: 'Invalid event' }, { status: 400 });
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// We don't want failures in third party services to prevent us from
|
|
127
|
-
// tracking analytics events, so we'll catch any errors and log them
|
|
128
|
-
// and continue with an 'Unknown' country code.
|
|
129
|
-
let countryISOCode = 'Unknown';
|
|
130
|
-
try {
|
|
131
|
-
const ip = await fetch('https://api64.ipify.org').then((res) =>
|
|
132
|
-
res.text(),
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
if (!ip) {
|
|
136
|
-
throw new Error('Could not fetch IP address');
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const geoData = (await fetch(`http://ip-api.com/json/${ip}`).then(
|
|
140
|
-
(res) => res.json(),
|
|
141
|
-
)) as GeoData;
|
|
142
|
-
|
|
143
|
-
if (geoData.status === 'success') {
|
|
144
|
-
countryISOCode = geoData.countryCode;
|
|
145
|
-
} else {
|
|
146
|
-
throw new Error(geoData.message);
|
|
147
|
-
}
|
|
148
|
-
} catch (e) {
|
|
149
|
-
// eslint-disable-next-line no-console
|
|
150
|
-
console.error('Geolocation failed:', e);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const analyticsEvent: analyticsEvent = {
|
|
154
|
-
...trackableEvent.data,
|
|
155
|
-
installationId,
|
|
156
|
-
countryISOCode,
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// Forward to backend
|
|
160
|
-
const response = await fetch(`${platformUrl}/api/event`, {
|
|
161
|
-
keepalive: true,
|
|
162
|
-
method: 'POST',
|
|
163
|
-
headers: {
|
|
164
|
-
'Content-Type': 'application/json',
|
|
165
|
-
},
|
|
166
|
-
body: JSON.stringify(analyticsEvent),
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
if (!response.ok) {
|
|
170
|
-
let error = `Analytics platform returned an unexpected error: ${response.statusText}`;
|
|
171
|
-
|
|
172
|
-
if (response.status === 400) {
|
|
173
|
-
error = `Analytics platform rejected the event as invalid. Please check the event schema`;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (response.status === 404) {
|
|
177
|
-
error = `Analytics platform could not be reached. Please specify a valid platform URL, or check that the platform is online.`;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (response.status === 500) {
|
|
181
|
-
error = `Analytics platform returned an internal server error. Please check the platform logs.`;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// eslint-disable-next-line no-console
|
|
185
|
-
console.info(`⚠️ Analytics platform rejected event: ${error}`);
|
|
186
|
-
return Response.json(
|
|
187
|
-
{
|
|
188
|
-
error,
|
|
189
|
-
},
|
|
190
|
-
{ status: 500 },
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
// eslint-disable-next-line no-console
|
|
194
|
-
console.info('🚀 Analytics event sent to platform!');
|
|
195
|
-
return Response.json({ message: 'Event forwarded successfully' });
|
|
196
|
-
} catch (e) {
|
|
197
|
-
const error = ensureError(e);
|
|
198
|
-
// eslint-disable-next-line no-console
|
|
199
|
-
console.info('🚫 Internal error with sending analytics event.');
|
|
200
|
-
|
|
201
|
-
return Response.json(
|
|
202
|
-
{ error: `Error in analytics route handler: ${error.message}` },
|
|
203
|
-
{ status: 500 },
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
export const makeEventTracker =
|
|
210
|
-
(options?: { endpoint?: string }) =>
|
|
211
|
-
async (
|
|
212
|
-
event: RawEvent,
|
|
213
|
-
): Promise<{
|
|
214
|
-
error: string | null;
|
|
215
|
-
success: boolean;
|
|
216
|
-
}> => {
|
|
217
|
-
// We use a relative path by default, which should automatically use the
|
|
218
|
-
// same origin as the page that is sending the event.
|
|
219
|
-
const endpoint = options?.endpoint ?? '/api/analytics';
|
|
220
|
-
|
|
221
|
-
const eventWithTimeStamp: TrackableEvent = {
|
|
222
|
-
...event,
|
|
223
|
-
timestamp: new Date().toJSON(),
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
const response = await fetch(endpoint, {
|
|
228
|
-
method: 'POST',
|
|
229
|
-
keepalive: true,
|
|
230
|
-
body: JSON.stringify(eventWithTimeStamp),
|
|
231
|
-
headers: {
|
|
232
|
-
'Content-Type': 'application/json',
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
if (!response.ok) {
|
|
237
|
-
if (response.status === 404) {
|
|
238
|
-
return {
|
|
239
|
-
error: `Analytics endpoint not found, did you forget to add the route?`,
|
|
240
|
-
success: false,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// createRouteHandler will return a 400 if the event failed schema validation.
|
|
245
|
-
if (response.status === 400) {
|
|
246
|
-
return {
|
|
247
|
-
error: `Invalid event sent to analytics endpoint: ${response.statusText}`,
|
|
248
|
-
success: false,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// createRouteHandler will return a 500 for all error states
|
|
253
|
-
return {
|
|
254
|
-
error: `Internal server error when sending analytics event: ${response.statusText}. Check the route handler implementation.`,
|
|
255
|
-
success: false,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
51
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
52
|
+
// Re-export config helpers
|
|
53
|
+
export { defaultConfig, isDisabledByEnv, mergeConfig } from "./config";
|
|
54
|
+
export { useAnalytics, useFeatureFlag, useFeatureFlagValue } from "./hooks";
|
|
55
|
+
|
|
56
|
+
// Re-export provider and hooks for client-side usage
|
|
57
|
+
export { AnalyticsProvider, type AnalyticsProviderProps } from "./provider";
|
|
58
|
+
// Re-export types
|
|
59
|
+
export type {
|
|
60
|
+
Analytics,
|
|
61
|
+
AnalyticsConfig,
|
|
62
|
+
ErrorProperties,
|
|
63
|
+
EventProperties,
|
|
64
|
+
EventType,
|
|
65
|
+
} from "./types";
|
|
66
|
+
export { eventTypes, legacyEventTypeMap } from "./types";
|
|
67
|
+
|
|
68
|
+
// Re-export utilities
|
|
69
|
+
export { ensureError } from "./utils";
|
|
70
|
+
|
|
71
|
+
// Note: Server-side exports are in a separate entry point
|
|
72
|
+
// Import from '@codaco/analytics/server' for server-side usage
|