@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/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/utils.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server';\nimport { ensureError } from './utils';\nimport z from 'zod';\n\n// Todo: it would be great to work out a way to support arbitrary types here.\nexport const eventTypes = [\n 'AppSetup',\n 'ProtocolInstalled',\n 'InterviewStarted',\n 'InterviewCompleted',\n 'DataExported',\n] as const;\n\nconst EventSchema = z.object({\n type: z.enum(eventTypes),\n});\n\nconst ErrorSchema = z.object({\n type: z.literal('Error'),\n message: z.string(),\n name: z.string(),\n stack: z.string().optional(),\n cause: z.string().optional(),\n});\n\nconst SharedEventAndErrorSchema = z.object({\n metadata: z.record(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 SharedEventAndErrorSchema.merge(EventSchema),\n SharedEventAndErrorSchema.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 timestamp: z.string(),\n});\n\nexport const TrackableEventSchema = z.intersection(\n RawEventSchema,\n TrackablePropertiesSchema,\n);\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 installationId: z.string(),\n countryISOCode: 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(\n TrackableEventSchema,\n DispatchablePropertiesSchema,\n);\nexport type analyticsEvent = z.infer<typeof AnalyticsEventSchema>;\n\ntype GeoData = {\n status: 'success' | 'fail';\n countryCode: string;\n message: string;\n};\n\nexport const createRouteHandler = ({\n platformUrl = 'https://analytics.networkcanvas.com',\n installationId,\n disableAnalytics,\n}: {\n platformUrl?: string;\n installationId: string;\n disableAnalytics?: boolean;\n}) => {\n return async (request: NextRequest) => {\n try {\n const incomingEvent = (await request.json()) as unknown;\n\n // Check if analytics is disabled\n if (disableAnalytics) {\n // eslint-disable-next-line no-console\n console.info('🛑 Analytics disabled. Payload not sent.');\n try {\n // eslint-disable-next-line no-console\n console.info(\n 'Payload:',\n '\\n',\n JSON.stringify(incomingEvent, null, 2),\n );\n } catch (e) {\n // eslint-disable-next-line no-console\n console.error('Error stringifying payload:', e);\n }\n\n return NextResponse.json(\n { message: 'Analytics disabled' },\n { status: 200 },\n );\n }\n\n // Validate the event\n const trackableEvent = TrackableEventSchema.safeParse(incomingEvent);\n\n if (!trackableEvent.success) {\n // eslint-disable-next-line no-console\n console.error('Invalid event:', trackableEvent.error);\n return NextResponse.json({ error: 'Invalid event' }, { status: 400 });\n }\n\n // We don't want failures in third party services to prevent us from\n // tracking analytics events, so we'll catch any errors and log them\n // and continue with an 'Unknown' country code.\n let countryISOCode = 'Unknown';\n try {\n const ip = await fetch('https://api64.ipify.org').then((res) =>\n res.text(),\n );\n\n if (!ip) {\n throw new Error('Could not fetch IP address');\n }\n\n const geoData = (await fetch(`http://ip-api.com/json/${ip}`).then(\n (res) => res.json(),\n )) as GeoData;\n\n if (geoData.status === 'success') {\n countryISOCode = geoData.countryCode;\n } else {\n throw new Error(geoData.message);\n }\n } catch (e) {\n // eslint-disable-next-line no-console\n console.error('Geolocation failed:', e);\n }\n\n const analyticsEvent: analyticsEvent = {\n ...trackableEvent.data,\n installationId,\n countryISOCode,\n };\n\n // Forward to backend\n const response = await fetch(`${platformUrl}/api/event`, {\n keepalive: true,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(analyticsEvent),\n });\n\n if (!response.ok) {\n let error = `Analytics platform returned an unexpected error: ${response.statusText}`;\n\n if (response.status === 400) {\n error = `Analytics platform rejected the event as invalid. Please check the event schema`;\n }\n\n if (response.status === 404) {\n error = `Analytics platform could not be reached. Please specify a valid platform URL, or check that the platform is online.`;\n }\n\n if (response.status === 500) {\n error = `Analytics platform returned an internal server error. Please check the platform logs.`;\n }\n\n // eslint-disable-next-line no-console\n console.info(`⚠️ Analytics platform rejected event: ${error}`);\n return Response.json(\n {\n error,\n },\n { status: 500 },\n );\n }\n // eslint-disable-next-line no-console\n console.info('🚀 Analytics event sent to platform!');\n return Response.json({ message: 'Event forwarded successfully' });\n } catch (e) {\n const error = ensureError(e);\n // eslint-disable-next-line no-console\n console.info('🚫 Internal error with sending analytics event.');\n\n return Response.json(\n { error: `Error in analytics route handler: ${error.message}` },\n { status: 500 },\n );\n }\n };\n};\n\nexport const makeEventTracker =\n (options?: { endpoint?: string }) =>\n async (\n event: RawEvent,\n ): Promise<{\n error: string | null;\n success: boolean;\n }> => {\n // We use a relative path by default, which should automatically use the\n // same origin as the page that is sending the event.\n const endpoint = options?.endpoint ?? '/api/analytics';\n\n const eventWithTimeStamp: TrackableEvent = {\n ...event,\n timestamp: new Date().toJSON(),\n };\n\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n keepalive: true,\n body: JSON.stringify(eventWithTimeStamp),\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n return {\n error: `Analytics endpoint not found, did you forget to add the route?`,\n success: false,\n };\n }\n\n // createRouteHandler will return a 400 if the event failed schema validation.\n if (response.status === 400) {\n return {\n error: `Invalid event sent to analytics endpoint: ${response.statusText}`,\n success: false,\n };\n }\n\n // createRouteHandler will return a 500 for all error states\n return {\n error: `Internal server error when sending analytics event: ${response.statusText}. Check the route handler implementation.`,\n success: false,\n };\n }\n\n return { error: null, success: true };\n } catch (e) {\n const error = ensureError(e);\n return {\n error: `Internal error when sending analytics event: ${error.message}`,\n success: false,\n };\n }\n };\n","// Helper function that ensures that a value is an Error\nexport function ensureError(value: unknown): Error {\n if (!value) return new Error('No value was thrown');\n\n if (value instanceof Error) return value;\n\n // Test if value inherits from Error\n if (Object.prototype.isPrototypeOf.call(value, Error))\n return value as Error & typeof value;\n\n let stringified = '[Unable to stringify the thrown value]';\n try {\n stringified = JSON.stringify(value);\n } catch (e) {\n // eslint-disable-next-line no-console\n console.error(e);\n }\n\n const error = new Error(\n `This value was thrown as is, not through an Error: ${stringified}`,\n );\n return 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 interface 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 interface 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 interface 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":[]}
@@ -0,0 +1,44 @@
1
+ import { A as AnalyticsConfig, a as Analytics } from './types-Ymgjicqi.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
+ interface 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
+ interface 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,38 +1,45 @@
1
1
  {
2
2
  "name": "@codaco/analytics",
3
- "version": "7.0.0",
4
- "packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589",
3
+ "version": "9.0.0",
5
4
  "type": "module",
6
5
  "main": "./dist/index.js",
7
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
+ },
8
17
  "author": "Complex Data Collective <hello@complexdatacollective.org>",
9
- "description": "Utilities for tracking analytics and error reporting in Fresco",
10
- "scripts": {
11
- "build": "tsup src/index.ts --format esm --dts --clean --sourcemap",
12
- "publish": "pnpm lint && pnpm build && npm publish --access public",
13
- "lint": "eslint .",
14
- "dev": "npm run build -- --watch"
18
+ "description": "PostHog analytics wrapper for Network Canvas applications with installation ID tracking and error reporting",
19
+ "publishConfig": {
20
+ "tag": "alpha"
15
21
  },
16
22
  "peerDependencies": {
17
- "next": "13 || 14 || 15"
23
+ "next": "^16.0.3",
24
+ "react": "^19.2.0"
18
25
  },
19
26
  "devDependencies": {
20
- "@codaco/eslint-config": "workspace:*",
21
- "@codaco/tsconfig": "workspace:*",
22
- "eslint": "^8.57.0",
23
- "prettier": "^3.2.5",
24
- "tsup": "^8.0.2",
25
- "typescript": "^5.4.5"
27
+ "@types/node": "^22.18.8",
28
+ "@types/react": "^19.2.5",
29
+ "tsup": "^8.5.1",
30
+ "typescript": "^5.9.3",
31
+ "vitest": "^4.0.9",
32
+ "@codaco/tsconfig": "0.1.0"
26
33
  },
27
34
  "dependencies": {
28
- "zod": "^3.23.8"
35
+ "posthog-js": "^1.214.4",
36
+ "zod": "^4.1.12"
29
37
  },
30
- "eslintConfig": {
31
- "root": true,
32
- "extends": [
33
- "@codaco/eslint-config/base",
34
- "@codaco/eslint-config/nextjs",
35
- "@codaco/eslint-config/react"
36
- ]
38
+ "scripts": {
39
+ "build": "tsup src/index.ts src/server.ts --format esm --dts --clean --sourcemap",
40
+ "dev": "npm run build -- --watch",
41
+ "test": "vitest run",
42
+ "test:watch": "vitest",
43
+ "typecheck": "tsc --noEmit"
37
44
  }
38
- }
45
+ }