@codaco/analytics 7.0.0 → 8.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 +7 -0
- package/dist/index.d.ts +33 -95
- package/dist/index.js +15 -38
- package/dist/index.js.map +1 -1
- package/package.json +11 -24
- package/src/index.ts +185 -212
- package/src/utils.ts +12 -16
- package/tsconfig.json +7 -7
- package/.turbo/turbo-build.log +0 -16
- package/.turbo/turbo-lint.log +0 -7
package/CHANGELOG.md
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -7,133 +7,71 @@ declare const eventTypes: readonly ["AppSetup", "ProtocolInstalled", "InterviewS
|
|
|
7
7
|
* general events or errors. We discriminate on the `type` property to determine
|
|
8
8
|
* which schema to use, and then merge the shared properties.
|
|
9
9
|
*/
|
|
10
|
-
declare const RawEventSchema: z.ZodDiscriminatedUnion<
|
|
10
|
+
declare const RawEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
11
11
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}>, z.ZodObject<z.objectUtil.extendShape<{
|
|
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<{
|
|
21
20
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
22
|
-
}, {
|
|
23
21
|
type: z.ZodLiteral<"Error">;
|
|
24
22
|
message: z.ZodString;
|
|
25
23
|
name: z.ZodString;
|
|
26
24
|
stack: z.ZodOptional<z.ZodString>;
|
|
27
25
|
cause: z.ZodOptional<z.ZodString>;
|
|
28
|
-
}
|
|
29
|
-
type: "Error";
|
|
30
|
-
message: string;
|
|
31
|
-
name: string;
|
|
32
|
-
stack?: string | undefined;
|
|
33
|
-
cause?: string | undefined;
|
|
34
|
-
metadata?: Record<string, unknown> | undefined;
|
|
35
|
-
}, {
|
|
36
|
-
type: "Error";
|
|
37
|
-
message: string;
|
|
38
|
-
name: string;
|
|
39
|
-
stack?: string | undefined;
|
|
40
|
-
cause?: string | undefined;
|
|
41
|
-
metadata?: Record<string, unknown> | undefined;
|
|
42
|
-
}>]>;
|
|
26
|
+
}, z.core.$strip>], "type">;
|
|
43
27
|
type RawEvent = z.infer<typeof RawEventSchema>;
|
|
44
|
-
declare const TrackableEventSchema: z.ZodIntersection<z.ZodDiscriminatedUnion<
|
|
28
|
+
declare const TrackableEventSchema: z.ZodIntersection<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
45
29
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}>, z.ZodObject<z.objectUtil.extendShape<{
|
|
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<{
|
|
55
38
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
56
|
-
}, {
|
|
57
39
|
type: z.ZodLiteral<"Error">;
|
|
58
40
|
message: z.ZodString;
|
|
59
41
|
name: z.ZodString;
|
|
60
42
|
stack: z.ZodOptional<z.ZodString>;
|
|
61
43
|
cause: z.ZodOptional<z.ZodString>;
|
|
62
|
-
}
|
|
63
|
-
type: "Error";
|
|
64
|
-
message: string;
|
|
65
|
-
name: string;
|
|
66
|
-
stack?: string | undefined;
|
|
67
|
-
cause?: string | undefined;
|
|
68
|
-
metadata?: Record<string, unknown> | undefined;
|
|
69
|
-
}, {
|
|
70
|
-
type: "Error";
|
|
71
|
-
message: string;
|
|
72
|
-
name: string;
|
|
73
|
-
stack?: string | undefined;
|
|
74
|
-
cause?: string | undefined;
|
|
75
|
-
metadata?: Record<string, unknown> | undefined;
|
|
76
|
-
}>]>, z.ZodObject<{
|
|
44
|
+
}, z.core.$strip>], "type">, z.ZodObject<{
|
|
77
45
|
timestamp: z.ZodString;
|
|
78
|
-
},
|
|
79
|
-
timestamp: string;
|
|
80
|
-
}, {
|
|
81
|
-
timestamp: string;
|
|
82
|
-
}>>;
|
|
46
|
+
}, z.core.$strip>>;
|
|
83
47
|
type TrackableEvent = z.infer<typeof TrackableEventSchema>;
|
|
84
48
|
/**
|
|
85
49
|
* The final schema for an analytics event. This is the schema that is used to
|
|
86
50
|
* validate the event before it is inserted into the database. It is the
|
|
87
51
|
* intersection of the trackable event and the dispatchable properties.
|
|
88
52
|
*/
|
|
89
|
-
declare const AnalyticsEventSchema: z.ZodIntersection<z.ZodIntersection<z.ZodDiscriminatedUnion<
|
|
53
|
+
declare const AnalyticsEventSchema: z.ZodIntersection<z.ZodIntersection<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
90
54
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}>, z.ZodObject<z.objectUtil.extendShape<{
|
|
55
|
+
type: z.ZodEnum<{
|
|
56
|
+
AppSetup: "AppSetup";
|
|
57
|
+
ProtocolInstalled: "ProtocolInstalled";
|
|
58
|
+
InterviewStarted: "InterviewStarted";
|
|
59
|
+
InterviewCompleted: "InterviewCompleted";
|
|
60
|
+
DataExported: "DataExported";
|
|
61
|
+
}>;
|
|
62
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
100
63
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
101
|
-
}, {
|
|
102
64
|
type: z.ZodLiteral<"Error">;
|
|
103
65
|
message: z.ZodString;
|
|
104
66
|
name: z.ZodString;
|
|
105
67
|
stack: z.ZodOptional<z.ZodString>;
|
|
106
68
|
cause: z.ZodOptional<z.ZodString>;
|
|
107
|
-
}
|
|
108
|
-
type: "Error";
|
|
109
|
-
message: string;
|
|
110
|
-
name: string;
|
|
111
|
-
stack?: string | undefined;
|
|
112
|
-
cause?: string | undefined;
|
|
113
|
-
metadata?: Record<string, unknown> | undefined;
|
|
114
|
-
}, {
|
|
115
|
-
type: "Error";
|
|
116
|
-
message: string;
|
|
117
|
-
name: string;
|
|
118
|
-
stack?: string | undefined;
|
|
119
|
-
cause?: string | undefined;
|
|
120
|
-
metadata?: Record<string, unknown> | undefined;
|
|
121
|
-
}>]>, z.ZodObject<{
|
|
69
|
+
}, z.core.$strip>], "type">, z.ZodObject<{
|
|
122
70
|
timestamp: z.ZodString;
|
|
123
|
-
},
|
|
124
|
-
timestamp: string;
|
|
125
|
-
}, {
|
|
126
|
-
timestamp: string;
|
|
127
|
-
}>>, z.ZodObject<{
|
|
71
|
+
}, z.core.$strip>>, z.ZodObject<{
|
|
128
72
|
installationId: z.ZodString;
|
|
129
73
|
countryISOCode: z.ZodString;
|
|
130
|
-
},
|
|
131
|
-
installationId: string;
|
|
132
|
-
countryISOCode: string;
|
|
133
|
-
}, {
|
|
134
|
-
installationId: string;
|
|
135
|
-
countryISOCode: string;
|
|
136
|
-
}>>;
|
|
74
|
+
}, z.core.$strip>>;
|
|
137
75
|
type analyticsEvent = z.infer<typeof AnalyticsEventSchema>;
|
|
138
76
|
declare const createRouteHandler: ({ platformUrl, installationId, disableAnalytics, }: {
|
|
139
77
|
platformUrl?: string;
|
package/dist/index.js
CHANGED
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
|
+
import z from "zod";
|
|
3
4
|
|
|
4
5
|
// src/utils.ts
|
|
5
6
|
function ensureError(value) {
|
|
6
7
|
if (!value) return new Error("No value was thrown");
|
|
7
8
|
if (value instanceof Error) return value;
|
|
8
|
-
if (Object.prototype.isPrototypeOf.call(value, Error))
|
|
9
|
-
return value;
|
|
9
|
+
if (Object.prototype.isPrototypeOf.call(value, Error)) return value;
|
|
10
10
|
let stringified = "[Unable to stringify the thrown value]";
|
|
11
11
|
try {
|
|
12
12
|
stringified = JSON.stringify(value);
|
|
13
13
|
} catch (e) {
|
|
14
14
|
console.error(e);
|
|
15
15
|
}
|
|
16
|
-
const error = new Error(
|
|
17
|
-
`This value was thrown as is, not through an Error: ${stringified}`
|
|
18
|
-
);
|
|
16
|
+
const error = new Error(`This value was thrown as is, not through an Error: ${stringified}`);
|
|
19
17
|
return error;
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
// src/index.ts
|
|
23
|
-
import z from "zod";
|
|
24
21
|
var eventTypes = [
|
|
25
22
|
"AppSetup",
|
|
26
23
|
"ProtocolInstalled",
|
|
@@ -39,7 +36,7 @@ var ErrorSchema = z.object({
|
|
|
39
36
|
cause: z.string().optional()
|
|
40
37
|
});
|
|
41
38
|
var SharedEventAndErrorSchema = z.object({
|
|
42
|
-
metadata: z.record(z.unknown()).optional()
|
|
39
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
43
40
|
});
|
|
44
41
|
var RawEventSchema = z.discriminatedUnion("type", [
|
|
45
42
|
SharedEventAndErrorSchema.merge(EventSchema),
|
|
@@ -48,18 +45,12 @@ var RawEventSchema = z.discriminatedUnion("type", [
|
|
|
48
45
|
var TrackablePropertiesSchema = z.object({
|
|
49
46
|
timestamp: z.string()
|
|
50
47
|
});
|
|
51
|
-
var TrackableEventSchema = z.intersection(
|
|
52
|
-
RawEventSchema,
|
|
53
|
-
TrackablePropertiesSchema
|
|
54
|
-
);
|
|
48
|
+
var TrackableEventSchema = z.intersection(RawEventSchema, TrackablePropertiesSchema);
|
|
55
49
|
var DispatchablePropertiesSchema = z.object({
|
|
56
50
|
installationId: z.string(),
|
|
57
51
|
countryISOCode: z.string()
|
|
58
52
|
});
|
|
59
|
-
var AnalyticsEventSchema = z.intersection(
|
|
60
|
-
TrackableEventSchema,
|
|
61
|
-
DispatchablePropertiesSchema
|
|
62
|
-
);
|
|
53
|
+
var AnalyticsEventSchema = z.intersection(TrackableEventSchema, DispatchablePropertiesSchema);
|
|
63
54
|
var createRouteHandler = ({
|
|
64
55
|
platformUrl = "https://analytics.networkcanvas.com",
|
|
65
56
|
installationId,
|
|
@@ -71,18 +62,11 @@ var createRouteHandler = ({
|
|
|
71
62
|
if (disableAnalytics) {
|
|
72
63
|
console.info("\u{1F6D1} Analytics disabled. Payload not sent.");
|
|
73
64
|
try {
|
|
74
|
-
console.info(
|
|
75
|
-
"Payload:",
|
|
76
|
-
"\n",
|
|
77
|
-
JSON.stringify(incomingEvent, null, 2)
|
|
78
|
-
);
|
|
65
|
+
console.info("Payload:", "\n", JSON.stringify(incomingEvent, null, 2));
|
|
79
66
|
} catch (e) {
|
|
80
67
|
console.error("Error stringifying payload:", e);
|
|
81
68
|
}
|
|
82
|
-
return NextResponse.json(
|
|
83
|
-
{ message: "Analytics disabled" },
|
|
84
|
-
{ status: 200 }
|
|
85
|
-
);
|
|
69
|
+
return NextResponse.json({ message: "Analytics disabled" }, { status: 200 });
|
|
86
70
|
}
|
|
87
71
|
const trackableEvent = TrackableEventSchema.safeParse(incomingEvent);
|
|
88
72
|
if (!trackableEvent.success) {
|
|
@@ -91,15 +75,11 @@ var createRouteHandler = ({
|
|
|
91
75
|
}
|
|
92
76
|
let countryISOCode = "Unknown";
|
|
93
77
|
try {
|
|
94
|
-
const ip = await fetch("https://api64.ipify.org").then(
|
|
95
|
-
(res) => res.text()
|
|
96
|
-
);
|
|
78
|
+
const ip = await fetch("https://api64.ipify.org").then((res) => res.text());
|
|
97
79
|
if (!ip) {
|
|
98
80
|
throw new Error("Could not fetch IP address");
|
|
99
81
|
}
|
|
100
|
-
const geoData = await fetch(`http://ip-api.com/json/${ip}`).then(
|
|
101
|
-
(res) => res.json()
|
|
102
|
-
);
|
|
82
|
+
const geoData = await fetch(`http://ip-api.com/json/${ip}`).then((res) => res.json());
|
|
103
83
|
if (geoData.status === "success") {
|
|
104
84
|
countryISOCode = geoData.countryCode;
|
|
105
85
|
} else {
|
|
@@ -124,13 +104,13 @@ var createRouteHandler = ({
|
|
|
124
104
|
if (!response.ok) {
|
|
125
105
|
let error = `Analytics platform returned an unexpected error: ${response.statusText}`;
|
|
126
106
|
if (response.status === 400) {
|
|
127
|
-
error =
|
|
107
|
+
error = "Analytics platform rejected the event as invalid. Please check the event schema";
|
|
128
108
|
}
|
|
129
109
|
if (response.status === 404) {
|
|
130
|
-
error =
|
|
110
|
+
error = "Analytics platform could not be reached. Please specify a valid platform URL, or check that the platform is online.";
|
|
131
111
|
}
|
|
132
112
|
if (response.status === 500) {
|
|
133
|
-
error =
|
|
113
|
+
error = "Analytics platform returned an internal server error. Please check the platform logs.";
|
|
134
114
|
}
|
|
135
115
|
console.info(`\u26A0\uFE0F Analytics platform rejected event: ${error}`);
|
|
136
116
|
return Response.json(
|
|
@@ -145,10 +125,7 @@ var createRouteHandler = ({
|
|
|
145
125
|
} catch (e) {
|
|
146
126
|
const error = ensureError(e);
|
|
147
127
|
console.info("\u{1F6AB} Internal error with sending analytics event.");
|
|
148
|
-
return Response.json(
|
|
149
|
-
{ error: `Error in analytics route handler: ${error.message}` },
|
|
150
|
-
{ status: 500 }
|
|
151
|
-
);
|
|
128
|
+
return Response.json({ error: `Error in analytics route handler: ${error.message}` }, { status: 500 });
|
|
152
129
|
}
|
|
153
130
|
};
|
|
154
131
|
};
|
|
@@ -170,7 +147,7 @@ var makeEventTracker = (options) => async (event) => {
|
|
|
170
147
|
if (!response.ok) {
|
|
171
148
|
if (response.status === 404) {
|
|
172
149
|
return {
|
|
173
|
-
error:
|
|
150
|
+
error: "Analytics endpoint not found, did you forget to add the route?",
|
|
174
151
|
success: false
|
|
175
152
|
};
|
|
176
153
|
}
|
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/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":[]}
|
package/package.json
CHANGED
|
@@ -1,38 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codaco/analytics",
|
|
3
|
-
"version": "
|
|
4
|
-
"packageManager": "pnpm@8.15.5+sha256.4b4efa12490e5055d59b9b9fc9438b7d581a6b7af3b5675eb5c5f447cee1a589",
|
|
3
|
+
"version": "8.0.0",
|
|
5
4
|
"type": "module",
|
|
6
5
|
"main": "./dist/index.js",
|
|
7
6
|
"types": "./dist/index.d.ts",
|
|
8
7
|
"author": "Complex Data Collective <hello@complexdatacollective.org>",
|
|
9
8
|
"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"
|
|
15
|
-
},
|
|
16
9
|
"peerDependencies": {
|
|
17
|
-
"next": "
|
|
10
|
+
"next": "15.4.7"
|
|
18
11
|
},
|
|
19
12
|
"devDependencies": {
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"prettier": "^3.2.5",
|
|
24
|
-
"tsup": "^8.0.2",
|
|
25
|
-
"typescript": "^5.4.5"
|
|
13
|
+
"tsup": "^8.4.0",
|
|
14
|
+
"typescript": "^5.9.2",
|
|
15
|
+
"@codaco/tsconfig": "0.1.0"
|
|
26
16
|
},
|
|
27
17
|
"dependencies": {
|
|
28
|
-
"zod": "^
|
|
18
|
+
"zod": "^4.1.5"
|
|
29
19
|
},
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
"@codaco/eslint-config/nextjs",
|
|
35
|
-
"@codaco/eslint-config/react"
|
|
36
|
-
]
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup src/index.ts --format esm --dts --clean --sourcemap",
|
|
22
|
+
"dev": "npm run build -- --watch",
|
|
23
|
+
"typecheck": "tsc --noEmit"
|
|
37
24
|
}
|
|
38
|
-
}
|
|
25
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { type NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { ensureError } from "./utils";
|
|
4
4
|
|
|
5
5
|
// Todo: it would be great to work out a way to support arbitrary types here.
|
|
6
6
|
export const eventTypes = [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
"AppSetup",
|
|
8
|
+
"ProtocolInstalled",
|
|
9
|
+
"InterviewStarted",
|
|
10
|
+
"InterviewCompleted",
|
|
11
|
+
"DataExported",
|
|
12
12
|
] as const;
|
|
13
13
|
|
|
14
14
|
const EventSchema = z.object({
|
|
15
|
-
|
|
15
|
+
type: z.enum(eventTypes),
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
const ErrorSchema = z.object({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
type: z.literal("Error"),
|
|
20
|
+
message: z.string(),
|
|
21
|
+
name: z.string(),
|
|
22
|
+
stack: z.string().optional(),
|
|
23
|
+
cause: z.string().optional(),
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
const SharedEventAndErrorSchema = z.object({
|
|
27
|
-
|
|
27
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -32,9 +32,9 @@ const SharedEventAndErrorSchema = z.object({
|
|
|
32
32
|
* general events or errors. We discriminate on the `type` property to determine
|
|
33
33
|
* which schema to use, and then merge the shared properties.
|
|
34
34
|
*/
|
|
35
|
-
export const RawEventSchema = z.discriminatedUnion(
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
export const RawEventSchema = z.discriminatedUnion("type", [
|
|
36
|
+
SharedEventAndErrorSchema.merge(EventSchema),
|
|
37
|
+
SharedEventAndErrorSchema.merge(ErrorSchema),
|
|
38
38
|
]);
|
|
39
39
|
export type RawEvent = z.infer<typeof RawEventSchema>;
|
|
40
40
|
|
|
@@ -44,13 +44,10 @@ export type RawEvent = z.infer<typeof RawEventSchema>;
|
|
|
44
44
|
* due to network latency or processing time.
|
|
45
45
|
*/
|
|
46
46
|
const TrackablePropertiesSchema = z.object({
|
|
47
|
-
|
|
47
|
+
timestamp: z.string(),
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
export const TrackableEventSchema = z.intersection(
|
|
51
|
-
RawEventSchema,
|
|
52
|
-
TrackablePropertiesSchema,
|
|
53
|
-
);
|
|
50
|
+
export const TrackableEventSchema = z.intersection(RawEventSchema, TrackablePropertiesSchema);
|
|
54
51
|
export type TrackableEvent = z.infer<typeof TrackableEventSchema>;
|
|
55
52
|
|
|
56
53
|
/**
|
|
@@ -58,8 +55,8 @@ export type TrackableEvent = z.infer<typeof TrackableEventSchema>;
|
|
|
58
55
|
* handler injects the installationId and countryISOCode properties.
|
|
59
56
|
*/
|
|
60
57
|
const DispatchablePropertiesSchema = z.object({
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
installationId: z.string(),
|
|
59
|
+
countryISOCode: z.string(),
|
|
63
60
|
});
|
|
64
61
|
|
|
65
62
|
/**
|
|
@@ -67,201 +64,177 @@ const DispatchablePropertiesSchema = z.object({
|
|
|
67
64
|
* validate the event before it is inserted into the database. It is the
|
|
68
65
|
* intersection of the trackable event and the dispatchable properties.
|
|
69
66
|
*/
|
|
70
|
-
export const AnalyticsEventSchema = z.intersection(
|
|
71
|
-
TrackableEventSchema,
|
|
72
|
-
DispatchablePropertiesSchema,
|
|
73
|
-
);
|
|
67
|
+
export const AnalyticsEventSchema = z.intersection(TrackableEventSchema, DispatchablePropertiesSchema);
|
|
74
68
|
export type analyticsEvent = z.infer<typeof AnalyticsEventSchema>;
|
|
75
69
|
|
|
76
70
|
type GeoData = {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
status: "success" | "fail";
|
|
72
|
+
countryCode: string;
|
|
73
|
+
message: string;
|
|
80
74
|
};
|
|
81
75
|
|
|
82
76
|
export const createRouteHandler = ({
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
77
|
+
platformUrl = "https://analytics.networkcanvas.com",
|
|
78
|
+
installationId,
|
|
79
|
+
disableAnalytics,
|
|
86
80
|
}: {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
platformUrl?: string;
|
|
82
|
+
installationId: string;
|
|
83
|
+
disableAnalytics?: boolean;
|
|
90
84
|
}) => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
};
|
|
85
|
+
return async (request: NextRequest) => {
|
|
86
|
+
try {
|
|
87
|
+
const incomingEvent = (await request.json()) as unknown;
|
|
88
|
+
|
|
89
|
+
// Check if analytics is disabled
|
|
90
|
+
if (disableAnalytics) {
|
|
91
|
+
console.info("🛑 Analytics disabled. Payload not sent.");
|
|
92
|
+
try {
|
|
93
|
+
console.info("Payload:", "\n", JSON.stringify(incomingEvent, null, 2));
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error("Error stringifying payload:", e);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return NextResponse.json({ message: "Analytics disabled" }, { status: 200 });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Validate the event
|
|
102
|
+
const trackableEvent = TrackableEventSchema.safeParse(incomingEvent);
|
|
103
|
+
|
|
104
|
+
if (!trackableEvent.success) {
|
|
105
|
+
console.error("Invalid event:", trackableEvent.error);
|
|
106
|
+
return NextResponse.json({ error: "Invalid event" }, { status: 400 });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// We don't want failures in third party services to prevent us from
|
|
110
|
+
// tracking analytics events, so we'll catch any errors and log them
|
|
111
|
+
// and continue with an 'Unknown' country code.
|
|
112
|
+
let countryISOCode = "Unknown";
|
|
113
|
+
try {
|
|
114
|
+
const ip = await fetch("https://api64.ipify.org").then((res) => res.text());
|
|
115
|
+
|
|
116
|
+
if (!ip) {
|
|
117
|
+
throw new Error("Could not fetch IP address");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const geoData = (await fetch(`http://ip-api.com/json/${ip}`).then((res) => res.json())) as GeoData;
|
|
121
|
+
|
|
122
|
+
if (geoData.status === "success") {
|
|
123
|
+
countryISOCode = geoData.countryCode;
|
|
124
|
+
} else {
|
|
125
|
+
throw new Error(geoData.message);
|
|
126
|
+
}
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.error("Geolocation failed:", e);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const analyticsEvent: analyticsEvent = {
|
|
132
|
+
...trackableEvent.data,
|
|
133
|
+
installationId,
|
|
134
|
+
countryISOCode,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Forward to backend
|
|
138
|
+
const response = await fetch(`${platformUrl}/api/event`, {
|
|
139
|
+
keepalive: true,
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: {
|
|
142
|
+
"Content-Type": "application/json",
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify(analyticsEvent),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
let error = `Analytics platform returned an unexpected error: ${response.statusText}`;
|
|
149
|
+
|
|
150
|
+
if (response.status === 400) {
|
|
151
|
+
error = "Analytics platform rejected the event as invalid. Please check the event schema";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (response.status === 404) {
|
|
155
|
+
error =
|
|
156
|
+
"Analytics platform could not be reached. Please specify a valid platform URL, or check that the platform is online.";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (response.status === 500) {
|
|
160
|
+
error = "Analytics platform returned an internal server error. Please check the platform logs.";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.info(`⚠️ Analytics platform rejected event: ${error}`);
|
|
164
|
+
return Response.json(
|
|
165
|
+
{
|
|
166
|
+
error,
|
|
167
|
+
},
|
|
168
|
+
{ status: 500 },
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
console.info("🚀 Analytics event sent to platform!");
|
|
172
|
+
return Response.json({ message: "Event forwarded successfully" });
|
|
173
|
+
} catch (e) {
|
|
174
|
+
const error = ensureError(e);
|
|
175
|
+
console.info("🚫 Internal error with sending analytics event.");
|
|
176
|
+
|
|
177
|
+
return Response.json({ error: `Error in analytics route handler: ${error.message}` }, { status: 500 });
|
|
178
|
+
}
|
|
179
|
+
};
|
|
207
180
|
};
|
|
208
181
|
|
|
209
182
|
export const makeEventTracker =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
183
|
+
(options?: { endpoint?: string }) =>
|
|
184
|
+
async (
|
|
185
|
+
event: RawEvent,
|
|
186
|
+
): Promise<{
|
|
187
|
+
error: string | null;
|
|
188
|
+
success: boolean;
|
|
189
|
+
}> => {
|
|
190
|
+
// We use a relative path by default, which should automatically use the
|
|
191
|
+
// same origin as the page that is sending the event.
|
|
192
|
+
const endpoint = options?.endpoint ?? "/api/analytics";
|
|
193
|
+
|
|
194
|
+
const eventWithTimeStamp: TrackableEvent = {
|
|
195
|
+
...event,
|
|
196
|
+
timestamp: new Date().toJSON(),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const response = await fetch(endpoint, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
keepalive: true,
|
|
203
|
+
body: JSON.stringify(eventWithTimeStamp),
|
|
204
|
+
headers: {
|
|
205
|
+
"Content-Type": "application/json",
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
if (response.status === 404) {
|
|
211
|
+
return {
|
|
212
|
+
error: "Analytics endpoint not found, did you forget to add the route?",
|
|
213
|
+
success: false,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// createRouteHandler will return a 400 if the event failed schema validation.
|
|
218
|
+
if (response.status === 400) {
|
|
219
|
+
return {
|
|
220
|
+
error: `Invalid event sent to analytics endpoint: ${response.statusText}`,
|
|
221
|
+
success: false,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// createRouteHandler will return a 500 for all error states
|
|
226
|
+
return {
|
|
227
|
+
error: `Internal server error when sending analytics event: ${response.statusText}. Check the route handler implementation.`,
|
|
228
|
+
success: false,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { error: null, success: true };
|
|
233
|
+
} catch (e) {
|
|
234
|
+
const error = ensureError(e);
|
|
235
|
+
return {
|
|
236
|
+
error: `Internal error when sending analytics event: ${error.message}`,
|
|
237
|
+
success: false,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
};
|
package/src/utils.ts
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
// Helper function that ensures that a value is an Error
|
|
2
2
|
export function ensureError(value: unknown): Error {
|
|
3
|
-
|
|
3
|
+
if (!value) return new Error("No value was thrown");
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
if (value instanceof Error) return value;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return value as Error & typeof value;
|
|
7
|
+
// Test if value inherits from Error
|
|
8
|
+
if (Object.prototype.isPrototypeOf.call(value, Error)) return value as Error & typeof value;
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
10
|
+
let stringified = "[Unable to stringify the thrown value]";
|
|
11
|
+
try {
|
|
12
|
+
stringified = JSON.stringify(value);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
console.error(e);
|
|
15
|
+
}
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
);
|
|
22
|
-
return error;
|
|
17
|
+
const error = new Error(`This value was thrown as is, not through an Error: ${stringified}`);
|
|
18
|
+
return error;
|
|
23
19
|
}
|
package/tsconfig.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"extends": "@codaco/tsconfig/base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"baseUrl": ".",
|
|
5
|
+
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
|
6
|
+
},
|
|
7
|
+
"include": ["."],
|
|
8
|
+
"exclude": ["dist", "build", "node_modules"]
|
|
9
9
|
}
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
> @codaco/analytics@6.0.0 build /Users/buckhalt/Code/complexdatacollective/network-canvas-monorepo/packages/analytics
|
|
4
|
-
> tsup src/index.ts --format esm --dts --clean --sourcemap
|
|
5
|
-
|
|
6
|
-
[34mCLI[39m Building entry: src/index.ts
|
|
7
|
-
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
8
|
-
[34mCLI[39m tsup v8.0.2
|
|
9
|
-
[34mCLI[39m Target: es2022
|
|
10
|
-
[34mCLI[39m Cleaning output folder
|
|
11
|
-
[34mESM[39m Build start
|
|
12
|
-
[32mESM[39m [1mdist/index.js [22m[32m6.32 KB[39m
|
|
13
|
-
[32mESM[39m [1mdist/index.js.map [22m[32m13.22 KB[39m
|
|
14
|
-
[32mESM[39m ⚡️ Build success in 167ms
|
|
15
|
-
[34mDTS[39m Build start
|
|
16
|
-
[41m[30m ELIFECYCLE [39m[49m [31mCommand failed.[39m
|
package/.turbo/turbo-lint.log
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
> @codaco/analytics@7.0.0 lint /Users/buckhalt/Code/complexdatacollective/network-canvas-monorepo/packages/analytics
|
|
4
|
-
> eslint .
|
|
5
|
-
|
|
6
|
-
Warning: React version was set to "detect" in eslint-plugin-react settings, but the "react" package is not installed. Assuming latest React version for linting.
|
|
7
|
-
Pages directory cannot be found at /Users/buckhalt/Code/complexdatacollective/network-canvas-monorepo/packages/analytics/pages or /Users/buckhalt/Code/complexdatacollective/network-canvas-monorepo/packages/analytics/src/pages. If using a custom path, please configure with the `no-html-link-for-pages` rule in your eslint config file.
|