@codaco/analytics 6.0.0-alpha.3 → 6.0.0-alpha.5
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.d.ts +3 -3
- package/dist/index.js +1 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +9 -9
- package/src/utils.ts +0 -15
package/dist/index.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ import z from 'zod';
|
|
|
3
3
|
|
|
4
4
|
declare const eventTypes: readonly ["AppSetup", "ProtocolInstalled", "InterviewStarted", "InterviewCompleted", "DataExported"];
|
|
5
5
|
/**
|
|
6
|
-
* Raw events are the
|
|
7
|
-
* events or errors. We discriminate on the `type` property to determine
|
|
8
|
-
* schema to use, and then merge the shared properties.
|
|
6
|
+
* Raw events are the payload that is sent to trackEvent, which can be either
|
|
7
|
+
* general events or errors. We discriminate on the `type` property to determine
|
|
8
|
+
* which schema to use, and then merge the shared properties.
|
|
9
9
|
*/
|
|
10
10
|
declare const RawEventSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<z.objectUtil.extendShape<{
|
|
11
11
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
package/dist/index.js
CHANGED
|
@@ -21,13 +21,6 @@ function ensureError(value) {
|
|
|
21
21
|
);
|
|
22
22
|
return error;
|
|
23
23
|
}
|
|
24
|
-
function getBaseUrl() {
|
|
25
|
-
if (process.env.VERCEL_URL)
|
|
26
|
-
return `https://${process.env.VERCEL_URL}`;
|
|
27
|
-
if (process.env.PUBLIC_URL)
|
|
28
|
-
return process.env.PUBLIC_URL;
|
|
29
|
-
return `http://127.0.0.1:3000`;
|
|
30
|
-
}
|
|
31
24
|
var strictBooleanSchema = z.enum(["true", "false", "True", "False", "TRUE", "FALSE"]).default("false").transform(
|
|
32
25
|
(value) => value === "true" || value === "True" || value === "TRUE"
|
|
33
26
|
);
|
|
@@ -170,13 +163,12 @@ var createRouteHandler = ({
|
|
|
170
163
|
};
|
|
171
164
|
var makeEventTracker = (options) => async (event) => {
|
|
172
165
|
const endpoint = options?.endpoint ?? "/api/analytics";
|
|
173
|
-
const endpointWithHost = getBaseUrl() + endpoint;
|
|
174
166
|
const eventWithTimeStamp = {
|
|
175
167
|
...event,
|
|
176
168
|
timestamp: (/* @__PURE__ */ new Date()).toJSON()
|
|
177
169
|
};
|
|
178
170
|
try {
|
|
179
|
-
const response = await fetch(
|
|
171
|
+
const response = await fetch(endpoint, {
|
|
180
172
|
method: "POST",
|
|
181
173
|
keepalive: true,
|
|
182
174
|
body: JSON.stringify(eventWithTimeStamp),
|
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, getBaseUrl, strictBooleanSchema } 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 events that are sent trackEvent. They are either general\n * events or errors. We discriminate on the `type` property to determine which\n * 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. The\n * `trackEvent` 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}: {\n platformUrl?: string;\n installationId: string;\n}) => {\n return async (request: NextRequest) => {\n try {\n const incomingEvent = (await request.json()) as unknown;\n\n const disableAnalytics = strictBooleanSchema.parse(\n // eslint-disable-next-line no-process-env\n process.env.DISABLE_ANALYTICS,\n );\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 const endpoint = options?.endpoint ?? '/api/analytics';\n\n const endpointWithHost = getBaseUrl() + endpoint;\n\n const eventWithTimeStamp: TrackableEvent = {\n ...event,\n timestamp: new Date().toJSON(),\n };\n\n try {\n const response = await fetch(endpointWithHost, {\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","/* eslint-disable no-process-env */\n\nimport { z } from 'zod';\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\nexport function getBaseUrl() {\n if (process.env.VERCEL_URL)\n // reference for vercel.com\n return `https://${process.env.VERCEL_URL}`;\n\n if (process.env.PUBLIC_URL)\n // Manually set deployment URL from env\n return process.env.PUBLIC_URL;\n\n // assume localhost\n return `http://127.0.0.1:3000`;\n}\n\n// this is a workaround for this issue:https://github.com/colinhacks/zod/issues/1630\n// z.coerce.boolean() doesn't work as expected\nexport const strictBooleanSchema = z\n .enum(['true', 'false', 'True', 'False', 'TRUE', 'FALSE'])\n .default('false')\n .transform(\n (value) => value === 'true' || value === 'True' || value === 'TRUE',\n );\n"],"mappings":"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","names":["z"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/utils.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from 'next/server';\nimport { ensureError, strictBooleanSchema } 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}: {\n platformUrl?: string;\n installationId: string;\n}) => {\n return async (request: NextRequest) => {\n try {\n const incomingEvent = (await request.json()) as unknown;\n\n const disableAnalytics = strictBooleanSchema.parse(\n // eslint-disable-next-line no-process-env\n process.env.DISABLE_ANALYTICS,\n );\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","import { z } from 'zod';\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\n// this is a workaround for this issue:https://github.com/colinhacks/zod/issues/1630\n// z.coerce.boolean() doesn't work as expected\nexport const strictBooleanSchema = z\n .enum(['true', 'false', 'True', 'False', 'TRUE', 'FALSE'])\n .default('false')\n .transform(\n (value) => value === 'true' || value === 'True' || value === 'TRUE',\n );\n"],"mappings":";AAAA,SAAS,oBAAsC;;;ACA/C,SAAS,SAAS;AAGX,SAAS,YAAY,OAAuB;AACjD,MAAI,CAAC;AAAO,WAAO,IAAI,MAAM,qBAAqB;AAElD,MAAI,iBAAiB;AAAO,WAAO;AAGnC,MAAI,OAAO,UAAU,cAAc,KAAK,OAAO,KAAK;AAClD,WAAO;AAET,MAAI,cAAc;AAClB,MAAI;AACF,kBAAc,KAAK,UAAU,KAAK;AAAA,EACpC,SAAS,GAAG;AAEV,YAAQ,MAAM,CAAC;AAAA,EACjB;AAEA,QAAM,QAAQ,IAAI;AAAA,IAChB,sDAAsD,WAAW;AAAA,EACnE;AACA,SAAO;AACT;AAIO,IAAM,sBAAsB,EAChC,KAAK,CAAC,QAAQ,SAAS,QAAQ,SAAS,QAAQ,OAAO,CAAC,EACxD,QAAQ,OAAO,EACf;AAAA,EACC,CAAC,UAAU,UAAU,UAAU,UAAU,UAAU,UAAU;AAC/D;;;AD/BF,OAAOA,QAAO;AAGP,IAAM,aAAa;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,cAAcA,GAAE,OAAO;AAAA,EAC3B,MAAMA,GAAE,KAAK,UAAU;AACzB,CAAC;AAED,IAAM,cAAcA,GAAE,OAAO;AAAA,EAC3B,MAAMA,GAAE,QAAQ,OAAO;AAAA,EACvB,SAASA,GAAE,OAAO;AAAA,EAClB,MAAMA,GAAE,OAAO;AAAA,EACf,OAAOA,GAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,OAAOA,GAAE,OAAO,EAAE,SAAS;AAC7B,CAAC;AAED,IAAM,4BAA4BA,GAAE,OAAO;AAAA,EACzC,UAAUA,GAAE,OAAOA,GAAE,QAAQ,CAAC,EAAE,SAAS;AAC3C,CAAC;AAOM,IAAM,iBAAiBA,GAAE,mBAAmB,QAAQ;AAAA,EACzD,0BAA0B,MAAM,WAAW;AAAA,EAC3C,0BAA0B,MAAM,WAAW;AAC7C,CAAC;AAQD,IAAM,4BAA4BA,GAAE,OAAO;AAAA,EACzC,WAAWA,GAAE,OAAO;AACtB,CAAC;AAEM,IAAM,uBAAuBA,GAAE;AAAA,EACpC;AAAA,EACA;AACF;AAOA,IAAM,+BAA+BA,GAAE,OAAO;AAAA,EAC5C,gBAAgBA,GAAE,OAAO;AAAA,EACzB,gBAAgBA,GAAE,OAAO;AAC3B,CAAC;AAOM,IAAM,uBAAuBA,GAAE;AAAA,EACpC;AAAA,EACA;AACF;AASO,IAAM,qBAAqB,CAAC;AAAA,EACjC,cAAc;AAAA,EACd;AACF,MAGM;AACJ,SAAO,OAAO,YAAyB;AACrC,QAAI;AACF,YAAM,gBAAiB,MAAM,QAAQ,KAAK;AAE1C,YAAM,mBAAmB,oBAAoB;AAAA;AAAA,QAE3C,QAAQ,IAAI;AAAA,MACd;AAGA,UAAI,kBAAkB;AAEpB,gBAAQ,KAAK,iDAA0C;AACvD,YAAI;AAEF,kBAAQ;AAAA,YACN;AAAA,YACA;AAAA,YACA,KAAK,UAAU,eAAe,MAAM,CAAC;AAAA,UACvC;AAAA,QACF,SAAS,GAAG;AAEV,kBAAQ,MAAM,+BAA+B,CAAC;AAAA,QAChD;AAEA,eAAO,aAAa;AAAA,UAClB,EAAE,SAAS,qBAAqB;AAAA,UAChC,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AAGA,YAAM,iBAAiB,qBAAqB,UAAU,aAAa;AAEnE,UAAI,CAAC,eAAe,SAAS;AAE3B,gBAAQ,MAAM,kBAAkB,eAAe,KAAK;AACpD,eAAO,aAAa,KAAK,EAAE,OAAO,gBAAgB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,MACtE;AAKA,UAAI,iBAAiB;AACrB,UAAI;AACF,cAAM,KAAK,MAAM,MAAM,yBAAyB,EAAE;AAAA,UAAK,CAAC,QACtD,IAAI,KAAK;AAAA,QACX;AAEA,YAAI,CAAC,IAAI;AACP,gBAAM,IAAI,MAAM,4BAA4B;AAAA,QAC9C;AAEA,cAAM,UAAW,MAAM,MAAM,0BAA0B,EAAE,EAAE,EAAE;AAAA,UAC3D,CAAC,QAAQ,IAAI,KAAK;AAAA,QACpB;AAEA,YAAI,QAAQ,WAAW,WAAW;AAChC,2BAAiB,QAAQ;AAAA,QAC3B,OAAO;AACL,gBAAM,IAAI,MAAM,QAAQ,OAAO;AAAA,QACjC;AAAA,MACF,SAAS,GAAG;AAEV,gBAAQ,MAAM,uBAAuB,CAAC;AAAA,MACxC;AAEA,YAAM,iBAAiC;AAAA,QACrC,GAAG,eAAe;AAAA,QAClB;AAAA,QACA;AAAA,MACF;AAGA,YAAM,WAAW,MAAM,MAAM,GAAG,WAAW,cAAc;AAAA,QACvD,WAAW;AAAA,QACX,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,QAClB;AAAA,QACA,MAAM,KAAK,UAAU,cAAc;AAAA,MACrC,CAAC;AAED,UAAI,CAAC,SAAS,IAAI;AAChB,YAAI,QAAQ,oDAAoD,SAAS,UAAU;AAEnF,YAAI,SAAS,WAAW,KAAK;AAC3B,kBAAQ;AAAA,QACV;AAEA,YAAI,SAAS,WAAW,KAAK;AAC3B,kBAAQ;AAAA,QACV;AAEA,YAAI,SAAS,WAAW,KAAK;AAC3B,kBAAQ;AAAA,QACV;AAGA,gBAAQ,KAAK,mDAAyC,KAAK,EAAE;AAC7D,eAAO,SAAS;AAAA,UACd;AAAA,YACE;AAAA,UACF;AAAA,UACA,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AAEA,cAAQ,KAAK,6CAAsC;AACnD,aAAO,SAAS,KAAK,EAAE,SAAS,+BAA+B,CAAC;AAAA,IAClE,SAAS,GAAG;AACV,YAAM,QAAQ,YAAY,CAAC;AAE3B,cAAQ,KAAK,wDAAiD;AAE9D,aAAO,SAAS;AAAA,QACd,EAAE,OAAO,qCAAqC,MAAM,OAAO,GAAG;AAAA,QAC9D,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AACF;AAEO,IAAM,mBACX,CAAC,YACD,OACE,UAII;AAGJ,QAAM,WAAW,SAAS,YAAY;AAEtC,QAAM,qBAAqC;AAAA,IACzC,GAAG;AAAA,IACH,YAAW,oBAAI,KAAK,GAAE,OAAO;AAAA,EAC/B;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,UAAU;AAAA,MACrC,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,MAAM,KAAK,UAAU,kBAAkB;AAAA,MACvC,SAAS;AAAA,QACP,gBAAgB;AAAA,MAClB;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,UAAI,SAAS,WAAW,KAAK;AAC3B,eAAO;AAAA,UACL,OAAO;AAAA,UACP,SAAS;AAAA,QACX;AAAA,MACF;AAGA,UAAI,SAAS,WAAW,KAAK;AAC3B,eAAO;AAAA,UACL,OAAO,6CAA6C,SAAS,UAAU;AAAA,UACvE,SAAS;AAAA,QACX;AAAA,MACF;AAGA,aAAO;AAAA,QACL,OAAO,uDAAuD,SAAS,UAAU;AAAA,QACjF,SAAS;AAAA,MACX;AAAA,IACF;AAEA,WAAO,EAAE,OAAO,MAAM,SAAS,KAAK;AAAA,EACtC,SAAS,GAAG;AACV,UAAM,QAAQ,YAAY,CAAC;AAC3B,WAAO;AAAA,MACL,OAAO,gDAAgD,MAAM,OAAO;AAAA,MACpE,SAAS;AAAA,IACX;AAAA,EACF;AACF;","names":["z"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codaco/analytics",
|
|
3
|
-
"version": "6.0.0-alpha.
|
|
3
|
+
"version": "6.0.0-alpha.5",
|
|
4
4
|
"packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"description": "Utilities for tracking analytics and error reporting in Fresco",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsup src/index.ts --format esm --dts --clean --sourcemap",
|
|
12
|
+
"publish": "pnpm lint && pnpm build && npm publish --access public",
|
|
12
13
|
"lint": "eslint .",
|
|
13
14
|
"dev": "npm run build -- --watch"
|
|
14
15
|
},
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse, type NextRequest } from 'next/server';
|
|
2
|
-
import { ensureError,
|
|
2
|
+
import { ensureError, strictBooleanSchema } from './utils';
|
|
3
3
|
import z from 'zod';
|
|
4
4
|
|
|
5
5
|
// Todo: it would be great to work out a way to support arbitrary types here.
|
|
@@ -28,9 +28,9 @@ const SharedEventAndErrorSchema = z.object({
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Raw events are the
|
|
32
|
-
* events or errors. We discriminate on the `type` property to determine
|
|
33
|
-
* schema to use, and then merge the shared properties.
|
|
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
34
|
*/
|
|
35
35
|
export const RawEventSchema = z.discriminatedUnion('type', [
|
|
36
36
|
SharedEventAndErrorSchema.merge(EventSchema),
|
|
@@ -39,8 +39,8 @@ export const RawEventSchema = z.discriminatedUnion('type', [
|
|
|
39
39
|
export type RawEvent = z.infer<typeof RawEventSchema>;
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Trackable events are the events that are sent to the route handler
|
|
43
|
-
* `trackEvent
|
|
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
44
|
* due to network latency or processing time.
|
|
45
45
|
*/
|
|
46
46
|
const TrackablePropertiesSchema = z.object({
|
|
@@ -217,17 +217,17 @@ export const makeEventTracker =
|
|
|
217
217
|
error: string | null;
|
|
218
218
|
success: boolean;
|
|
219
219
|
}> => {
|
|
220
|
+
// We use a relative path by default, which should automatically use the
|
|
221
|
+
// same origin as the page that is sending the event.
|
|
220
222
|
const endpoint = options?.endpoint ?? '/api/analytics';
|
|
221
223
|
|
|
222
|
-
const endpointWithHost = getBaseUrl() + endpoint;
|
|
223
|
-
|
|
224
224
|
const eventWithTimeStamp: TrackableEvent = {
|
|
225
225
|
...event,
|
|
226
226
|
timestamp: new Date().toJSON(),
|
|
227
227
|
};
|
|
228
228
|
|
|
229
229
|
try {
|
|
230
|
-
const response = await fetch(
|
|
230
|
+
const response = await fetch(endpoint, {
|
|
231
231
|
method: 'POST',
|
|
232
232
|
keepalive: true,
|
|
233
233
|
body: JSON.stringify(eventWithTimeStamp),
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-process-env */
|
|
2
|
-
|
|
3
1
|
import { z } from 'zod';
|
|
4
2
|
|
|
5
3
|
// Helper function that ensures that a value is an Error
|
|
@@ -26,19 +24,6 @@ export function ensureError(value: unknown): Error {
|
|
|
26
24
|
return error;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
export function getBaseUrl() {
|
|
30
|
-
if (process.env.VERCEL_URL)
|
|
31
|
-
// reference for vercel.com
|
|
32
|
-
return `https://${process.env.VERCEL_URL}`;
|
|
33
|
-
|
|
34
|
-
if (process.env.PUBLIC_URL)
|
|
35
|
-
// Manually set deployment URL from env
|
|
36
|
-
return process.env.PUBLIC_URL;
|
|
37
|
-
|
|
38
|
-
// assume localhost
|
|
39
|
-
return `http://127.0.0.1:3000`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
27
|
// this is a workaround for this issue:https://github.com/colinhacks/zod/issues/1630
|
|
43
28
|
// z.coerce.boolean() doesn't work as expected
|
|
44
29
|
export const strictBooleanSchema = z
|