@codaco/analytics 4.0.0 → 5.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/.turbo/turbo-build.log +7 -7
- package/dist/{index.d.mts → index.d.ts} +57 -83
- package/dist/{index.mjs → index.js} +21 -16
- package/dist/index.js.map +1 -0
- package/package.json +4 -3
- package/src/index.ts +59 -43
- package/.turbo/turbo-test.log +0 -31
- package/dist/index.mjs.map +0 -1
- package/jest.config.js +0 -198
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @codaco/analytics@
|
|
3
|
+
> @codaco/analytics@5.0.0 build /Users/jmh629/Projects/error-analytics-microservice/packages/analytics
|
|
4
4
|
> tsup src/index.ts --format esm --dts --clean --sourcemap
|
|
5
5
|
|
|
6
6
|
[34mCLI[39m Building entry: src/index.ts
|
|
7
7
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
8
|
-
[34mCLI[39m tsup v7.
|
|
8
|
+
[34mCLI[39m tsup v7.3.0
|
|
9
9
|
[34mCLI[39m Target: es2022
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
|
-
[32mESM[39m [1mdist/index.
|
|
13
|
-
[32mESM[39m [1mdist/index.
|
|
14
|
-
[32mESM[39m ⚡️ Build success in
|
|
12
|
+
[32mESM[39m [1mdist/index.js [22m[32m5.77 KB[39m
|
|
13
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m11.87 KB[39m
|
|
14
|
+
[32mESM[39m ⚡️ Build success in 18ms
|
|
15
15
|
[34mDTS[39m Build start
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
17
|
-
[32mDTS[39m [1mdist/index.d.
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 907ms
|
|
17
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m5.64 KB[39m
|
|
@@ -3,6 +3,11 @@ import { WebServiceClient } from '@maxmind/geoip2-node';
|
|
|
3
3
|
import z from 'zod';
|
|
4
4
|
|
|
5
5
|
declare const eventTypes: readonly ["AppSetup", "ProtocolInstalled", "InterviewStarted", "InterviewCompleted", "DataExported"];
|
|
6
|
+
/**
|
|
7
|
+
* Raw events are the events that are sent trackEvent. They are either general
|
|
8
|
+
* events or errors. We discriminate on the `type` property to determine which
|
|
9
|
+
* schema to use, and then merge the shared properties.
|
|
10
|
+
*/
|
|
6
11
|
declare const RawEventSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
7
12
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
8
13
|
type: z.ZodEnum<["AppSetup", "ProtocolInstalled", "InterviewStarted", "InterviewCompleted", "DataExported"]>;
|
|
@@ -15,35 +20,24 @@ declare const RawEventSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
|
15
20
|
}>, z.ZodObject<{
|
|
16
21
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
17
22
|
type: z.ZodLiteral<"Error">;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}, "strict", z.ZodTypeAny, {
|
|
23
|
-
message: string;
|
|
24
|
-
name: string;
|
|
25
|
-
stack?: string | undefined;
|
|
26
|
-
}, {
|
|
27
|
-
message: string;
|
|
28
|
-
name: string;
|
|
29
|
-
stack?: string | undefined;
|
|
30
|
-
}>;
|
|
23
|
+
message: z.ZodString;
|
|
24
|
+
name: z.ZodString;
|
|
25
|
+
stack: z.ZodOptional<z.ZodString>;
|
|
26
|
+
cause: z.ZodOptional<z.ZodString>;
|
|
31
27
|
}, "strip", z.ZodTypeAny, {
|
|
32
28
|
type: "Error";
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
name: string;
|
|
36
|
-
stack?: string | undefined;
|
|
37
|
-
};
|
|
29
|
+
message: string;
|
|
30
|
+
name: string;
|
|
38
31
|
metadata?: Record<string, unknown> | undefined;
|
|
32
|
+
stack?: string | undefined;
|
|
33
|
+
cause?: string | undefined;
|
|
39
34
|
}, {
|
|
40
35
|
type: "Error";
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
name: string;
|
|
44
|
-
stack?: string | undefined;
|
|
45
|
-
};
|
|
36
|
+
message: string;
|
|
37
|
+
name: string;
|
|
46
38
|
metadata?: Record<string, unknown> | undefined;
|
|
39
|
+
stack?: string | undefined;
|
|
40
|
+
cause?: string | undefined;
|
|
47
41
|
}>]>;
|
|
48
42
|
type RawEvent = z.infer<typeof RawEventSchema>;
|
|
49
43
|
declare const TrackableEventSchema: z.ZodIntersection<z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
@@ -58,35 +52,24 @@ declare const TrackableEventSchema: z.ZodIntersection<z.ZodDiscriminatedUnion<"t
|
|
|
58
52
|
}>, z.ZodObject<{
|
|
59
53
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
60
54
|
type: z.ZodLiteral<"Error">;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}, "strict", z.ZodTypeAny, {
|
|
66
|
-
message: string;
|
|
67
|
-
name: string;
|
|
68
|
-
stack?: string | undefined;
|
|
69
|
-
}, {
|
|
70
|
-
message: string;
|
|
71
|
-
name: string;
|
|
72
|
-
stack?: string | undefined;
|
|
73
|
-
}>;
|
|
55
|
+
message: z.ZodString;
|
|
56
|
+
name: z.ZodString;
|
|
57
|
+
stack: z.ZodOptional<z.ZodString>;
|
|
58
|
+
cause: z.ZodOptional<z.ZodString>;
|
|
74
59
|
}, "strip", z.ZodTypeAny, {
|
|
75
60
|
type: "Error";
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
name: string;
|
|
79
|
-
stack?: string | undefined;
|
|
80
|
-
};
|
|
61
|
+
message: string;
|
|
62
|
+
name: string;
|
|
81
63
|
metadata?: Record<string, unknown> | undefined;
|
|
64
|
+
stack?: string | undefined;
|
|
65
|
+
cause?: string | undefined;
|
|
82
66
|
}, {
|
|
83
67
|
type: "Error";
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
name: string;
|
|
87
|
-
stack?: string | undefined;
|
|
88
|
-
};
|
|
68
|
+
message: string;
|
|
69
|
+
name: string;
|
|
89
70
|
metadata?: Record<string, unknown> | undefined;
|
|
71
|
+
stack?: string | undefined;
|
|
72
|
+
cause?: string | undefined;
|
|
90
73
|
}>]>, z.ZodObject<{
|
|
91
74
|
timestamp: z.ZodString;
|
|
92
75
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -95,7 +78,12 @@ declare const TrackableEventSchema: z.ZodIntersection<z.ZodDiscriminatedUnion<"t
|
|
|
95
78
|
timestamp: string;
|
|
96
79
|
}>>;
|
|
97
80
|
type TrackableEvent = z.infer<typeof TrackableEventSchema>;
|
|
98
|
-
|
|
81
|
+
/**
|
|
82
|
+
* The final schema for an analytics event. This is the schema that is used to
|
|
83
|
+
* validate the event before it is inserted into the database. It is the
|
|
84
|
+
* intersection of the trackable event and the dispatchable properties.
|
|
85
|
+
*/
|
|
86
|
+
declare const AnalyticsEventSchema: z.ZodIntersection<z.ZodIntersection<z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
99
87
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
100
88
|
type: z.ZodEnum<["AppSetup", "ProtocolInstalled", "InterviewStarted", "InterviewCompleted", "DataExported"]>;
|
|
101
89
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -107,35 +95,24 @@ declare const DispatchableEventSchema: z.ZodIntersection<z.ZodIntersection<z.Zod
|
|
|
107
95
|
}>, z.ZodObject<{
|
|
108
96
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
109
97
|
type: z.ZodLiteral<"Error">;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}, "strict", z.ZodTypeAny, {
|
|
115
|
-
message: string;
|
|
116
|
-
name: string;
|
|
117
|
-
stack?: string | undefined;
|
|
118
|
-
}, {
|
|
119
|
-
message: string;
|
|
120
|
-
name: string;
|
|
121
|
-
stack?: string | undefined;
|
|
122
|
-
}>;
|
|
98
|
+
message: z.ZodString;
|
|
99
|
+
name: z.ZodString;
|
|
100
|
+
stack: z.ZodOptional<z.ZodString>;
|
|
101
|
+
cause: z.ZodOptional<z.ZodString>;
|
|
123
102
|
}, "strip", z.ZodTypeAny, {
|
|
124
103
|
type: "Error";
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
name: string;
|
|
128
|
-
stack?: string | undefined;
|
|
129
|
-
};
|
|
104
|
+
message: string;
|
|
105
|
+
name: string;
|
|
130
106
|
metadata?: Record<string, unknown> | undefined;
|
|
107
|
+
stack?: string | undefined;
|
|
108
|
+
cause?: string | undefined;
|
|
131
109
|
}, {
|
|
132
110
|
type: "Error";
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
name: string;
|
|
136
|
-
stack?: string | undefined;
|
|
137
|
-
};
|
|
111
|
+
message: string;
|
|
112
|
+
name: string;
|
|
138
113
|
metadata?: Record<string, unknown> | undefined;
|
|
114
|
+
stack?: string | undefined;
|
|
115
|
+
cause?: string | undefined;
|
|
139
116
|
}>]>, z.ZodObject<{
|
|
140
117
|
timestamp: z.ZodString;
|
|
141
118
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -152,21 +129,18 @@ declare const DispatchableEventSchema: z.ZodIntersection<z.ZodIntersection<z.Zod
|
|
|
152
129
|
installationId: string;
|
|
153
130
|
countryISOCode: string;
|
|
154
131
|
}>>;
|
|
155
|
-
type
|
|
156
|
-
|
|
157
|
-
platformUrl?: string;
|
|
132
|
+
type analyticsEvent = z.infer<typeof AnalyticsEventSchema>;
|
|
133
|
+
declare const createRouteHandler: ({ platformUrl, installationId, maxMindClient, }: {
|
|
134
|
+
platformUrl?: string | undefined;
|
|
158
135
|
installationId: string;
|
|
159
136
|
maxMindClient: WebServiceClient;
|
|
160
|
-
}
|
|
161
|
-
declare const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
};
|
|
166
|
-
type EventTrackerReturn = {
|
|
137
|
+
}) => (request: NextRequest) => Promise<Response>;
|
|
138
|
+
declare const makeEventTracker: ({ enabled, endpoint, }: {
|
|
139
|
+
enabled?: boolean | undefined;
|
|
140
|
+
endpoint?: string | undefined;
|
|
141
|
+
}) => (event: RawEvent) => Promise<{
|
|
167
142
|
error: string | null;
|
|
168
143
|
success: boolean;
|
|
169
|
-
}
|
|
170
|
-
declare const makeEventTracker: ({ enabled, endpoint }: ConsumerConfiguration) => (event: RawEvent) => Promise<EventTrackerReturn>;
|
|
144
|
+
}>;
|
|
171
145
|
|
|
172
|
-
export {
|
|
146
|
+
export { AnalyticsEventSchema, type RawEvent, RawEventSchema, type TrackableEvent, TrackableEventSchema, type analyticsEvent, createRouteHandler, eventTypes, makeEventTracker };
|
|
@@ -35,19 +35,18 @@ var eventTypes = [
|
|
|
35
35
|
"InterviewCompleted",
|
|
36
36
|
"DataExported"
|
|
37
37
|
];
|
|
38
|
-
var SharedEventAndErrorSchema = z.object({
|
|
39
|
-
metadata: z.record(z.unknown()).optional()
|
|
40
|
-
});
|
|
41
38
|
var EventSchema = z.object({
|
|
42
39
|
type: z.enum(eventTypes)
|
|
43
40
|
});
|
|
44
41
|
var ErrorSchema = z.object({
|
|
45
42
|
type: z.literal("Error"),
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
message: z.string(),
|
|
44
|
+
name: z.string(),
|
|
45
|
+
stack: z.string().optional(),
|
|
46
|
+
cause: z.string().optional()
|
|
47
|
+
});
|
|
48
|
+
var SharedEventAndErrorSchema = z.object({
|
|
49
|
+
metadata: z.record(z.unknown()).optional()
|
|
51
50
|
});
|
|
52
51
|
var RawEventSchema = z.discriminatedUnion("type", [
|
|
53
52
|
SharedEventAndErrorSchema.merge(EventSchema),
|
|
@@ -64,7 +63,7 @@ var DispatchablePropertiesSchema = z.object({
|
|
|
64
63
|
installationId: z.string(),
|
|
65
64
|
countryISOCode: z.string()
|
|
66
65
|
});
|
|
67
|
-
var
|
|
66
|
+
var AnalyticsEventSchema = z.intersection(
|
|
68
67
|
TrackableEventSchema,
|
|
69
68
|
DispatchablePropertiesSchema
|
|
70
69
|
);
|
|
@@ -91,12 +90,15 @@ var createRouteHandler = ({
|
|
|
91
90
|
const ip = await fetch("https://api64.ipify.org").then(
|
|
92
91
|
(res) => res.text()
|
|
93
92
|
);
|
|
93
|
+
if (!ip) {
|
|
94
|
+
throw new Error("Could not fetch IP address");
|
|
95
|
+
}
|
|
94
96
|
const { country } = await maxMindClient.country(ip);
|
|
95
97
|
countryISOCode = country?.isoCode ?? "Unknown";
|
|
96
98
|
} catch (e) {
|
|
97
99
|
console.error("Geolocation failed:", e);
|
|
98
100
|
}
|
|
99
|
-
const
|
|
101
|
+
const analyticsEvent = {
|
|
100
102
|
...trackableEvent.data,
|
|
101
103
|
installationId,
|
|
102
104
|
countryISOCode
|
|
@@ -107,7 +109,7 @@ var createRouteHandler = ({
|
|
|
107
109
|
headers: {
|
|
108
110
|
"Content-Type": "application/json"
|
|
109
111
|
},
|
|
110
|
-
body: JSON.stringify(
|
|
112
|
+
body: JSON.stringify(analyticsEvent)
|
|
111
113
|
});
|
|
112
114
|
if (!response.ok) {
|
|
113
115
|
let error = `Analytics platform returned an unexpected error: ${response.statusText}`;
|
|
@@ -120,7 +122,7 @@ var createRouteHandler = ({
|
|
|
120
122
|
if (response.status === 500) {
|
|
121
123
|
error = `Analytics platform returned an internal server error. Please check the platform logs.`;
|
|
122
124
|
}
|
|
123
|
-
console.info(
|
|
125
|
+
console.info(`\u26A0\uFE0F Analytics platform rejected event: ${error}`);
|
|
124
126
|
return Response.json(
|
|
125
127
|
{
|
|
126
128
|
error
|
|
@@ -140,9 +142,12 @@ var createRouteHandler = ({
|
|
|
140
142
|
}
|
|
141
143
|
};
|
|
142
144
|
};
|
|
143
|
-
var makeEventTracker = ({
|
|
145
|
+
var makeEventTracker = ({
|
|
146
|
+
enabled = false,
|
|
147
|
+
endpoint = "/api/analytics"
|
|
148
|
+
}) => async (event) => {
|
|
144
149
|
if (!enabled) {
|
|
145
|
-
console.log("Analytics disabled
|
|
150
|
+
console.log("Analytics disabled - event not sent.");
|
|
146
151
|
return { error: null, success: true };
|
|
147
152
|
}
|
|
148
153
|
const endpointWithHost = getBaseUrl() + endpoint;
|
|
@@ -187,11 +192,11 @@ var makeEventTracker = ({ enabled = false, endpoint = "/api/analytics" }) => asy
|
|
|
187
192
|
}
|
|
188
193
|
};
|
|
189
194
|
export {
|
|
190
|
-
|
|
195
|
+
AnalyticsEventSchema,
|
|
191
196
|
RawEventSchema,
|
|
192
197
|
TrackableEventSchema,
|
|
193
198
|
createRouteHandler,
|
|
194
199
|
eventTypes,
|
|
195
200
|
makeEventTracker
|
|
196
201
|
};
|
|
197
|
-
//# sourceMappingURL=index.
|
|
202
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["// 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 (value.isPrototypeOf(Error)) 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 {}\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 (typeof window !== \"undefined\")\n // browser should use relative path\n return \"\";\n\n if (process.env.VERCEL_URL)\n // reference for vercel.com\n return `https://${process.env.VERCEL_URL}`;\n\n if (process.env.NEXT_PUBLIC_URL)\n // Manually set deployment URL from env\n return process.env.NEXT_PUBLIC_URL;\n\n // assume localhost\n return `http://127.0.0.1:3000`;\n}\n","import { type NextRequest } from \"next/server\";\nimport { WebServiceClient } from \"@maxmind/geoip2-node\";\nimport { ensureError, getBaseUrl } 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\nexport const createRouteHandler = ({\n platformUrl = \"https://analytics.networkcanvas.com\",\n installationId,\n maxMindClient,\n}: {\n platformUrl?: string;\n installationId: string;\n maxMindClient: WebServiceClient;\n}) => {\n return async (request: NextRequest) => {\n try {\n const incomingEvent = (await request.json()) as unknown;\n\n // Validate the event\n const trackableEvent = TrackableEventSchema.safeParse(incomingEvent);\n\n if (!trackableEvent.success) {\n console.error(\"Invalid event:\", trackableEvent.error);\n return new Response(JSON.stringify({ error: \"Invalid event\" }), {\n status: 400,\n headers: {\n \"Content-Type\": \"application/json\",\n },\n });\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 { country } = await maxMindClient.country(ip);\n countryISOCode = country?.isoCode ?? \"Unknown\";\n } catch (e) {\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 console.info(`⚠️ Analytics platform rejected event: ${error}`);\n return Response.json(\n {\n error,\n },\n { status: 500 }\n );\n }\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 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 ({\n enabled = false,\n endpoint = \"/api/analytics\",\n }: {\n enabled?: boolean;\n endpoint?: string;\n }) =>\n async (\n event: RawEvent\n ): Promise<{\n error: string | null;\n success: boolean;\n }> => {\n if (!enabled) {\n console.log(\"Analytics disabled - event not sent.\");\n return { error: null, success: true };\n }\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"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codaco/analytics",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
6
7
|
"author": "Complex Data Collective <developers@coda.co>",
|
|
7
8
|
"description": "Utilities for tracking analytics and error reporting in Fresco",
|
|
8
9
|
"scripts": {
|
package/src/index.ts
CHANGED
|
@@ -12,34 +12,38 @@ export const eventTypes = [
|
|
|
12
12
|
"DataExported",
|
|
13
13
|
] as const;
|
|
14
14
|
|
|
15
|
-
// Properties that everything has in common.
|
|
16
|
-
const SharedEventAndErrorSchema = z.object({
|
|
17
|
-
metadata: z.record(z.unknown()).optional(),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
15
|
const EventSchema = z.object({
|
|
21
16
|
type: z.enum(eventTypes),
|
|
22
17
|
});
|
|
23
18
|
|
|
24
19
|
const ErrorSchema = z.object({
|
|
25
20
|
type: z.literal("Error"),
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
message: z.string(),
|
|
22
|
+
name: z.string(),
|
|
23
|
+
stack: z.string().optional(),
|
|
24
|
+
cause: z.string().optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const SharedEventAndErrorSchema = z.object({
|
|
28
|
+
metadata: z.record(z.unknown()).optional(),
|
|
33
29
|
});
|
|
34
30
|
|
|
35
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Raw events are the events that are sent trackEvent. They are either general
|
|
33
|
+
* events or errors. We discriminate on the `type` property to determine which
|
|
34
|
+
* schema to use, and then merge the shared properties.
|
|
35
|
+
*/
|
|
36
36
|
export const RawEventSchema = z.discriminatedUnion("type", [
|
|
37
37
|
SharedEventAndErrorSchema.merge(EventSchema),
|
|
38
38
|
SharedEventAndErrorSchema.merge(ErrorSchema),
|
|
39
39
|
]);
|
|
40
40
|
export type RawEvent = z.infer<typeof RawEventSchema>;
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Trackable events are the events that are sent to the route handler. The
|
|
44
|
+
* `trackEvent` function adds the timestamp to ensure it is not inaccurate
|
|
45
|
+
* due to network latency or processing time.
|
|
46
|
+
*/
|
|
43
47
|
const TrackablePropertiesSchema = z.object({
|
|
44
48
|
timestamp: z.string(),
|
|
45
49
|
});
|
|
@@ -50,29 +54,35 @@ export const TrackableEventSchema = z.intersection(
|
|
|
50
54
|
);
|
|
51
55
|
export type TrackableEvent = z.infer<typeof TrackableEventSchema>;
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Dispatchable events are the events that are sent to the platform. The route
|
|
59
|
+
* handler injects the installationId and countryISOCode properties.
|
|
60
|
+
*/
|
|
54
61
|
const DispatchablePropertiesSchema = z.object({
|
|
55
62
|
installationId: z.string(),
|
|
56
63
|
countryISOCode: z.string(),
|
|
57
64
|
});
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The final schema for an analytics event. This is the schema that is used to
|
|
68
|
+
* validate the event before it is inserted into the database. It is the
|
|
69
|
+
* intersection of the trackable event and the dispatchable properties.
|
|
70
|
+
*/
|
|
71
|
+
export const AnalyticsEventSchema = z.intersection(
|
|
60
72
|
TrackableEventSchema,
|
|
61
73
|
DispatchablePropertiesSchema
|
|
62
74
|
);
|
|
63
|
-
export type
|
|
64
|
-
|
|
65
|
-
type RouteHandlerConfiguration = {
|
|
66
|
-
platformUrl?: string;
|
|
67
|
-
installationId: string;
|
|
68
|
-
maxMindClient: WebServiceClient;
|
|
69
|
-
};
|
|
75
|
+
export type analyticsEvent = z.infer<typeof AnalyticsEventSchema>;
|
|
70
76
|
|
|
71
77
|
export const createRouteHandler = ({
|
|
72
78
|
platformUrl = "https://analytics.networkcanvas.com",
|
|
73
79
|
installationId,
|
|
74
80
|
maxMindClient,
|
|
75
|
-
}:
|
|
81
|
+
}: {
|
|
82
|
+
platformUrl?: string;
|
|
83
|
+
installationId: string;
|
|
84
|
+
maxMindClient: WebServiceClient;
|
|
85
|
+
}) => {
|
|
76
86
|
return async (request: NextRequest) => {
|
|
77
87
|
try {
|
|
78
88
|
const incomingEvent = (await request.json()) as unknown;
|
|
@@ -91,32 +101,38 @@ export const createRouteHandler = ({
|
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
// We don't want failures in third party services to prevent us from
|
|
94
|
-
// tracking analytics events
|
|
104
|
+
// tracking analytics events, so we'll catch any errors and log them
|
|
105
|
+
// and continue with an 'Unknown' country code.
|
|
95
106
|
let countryISOCode = "Unknown";
|
|
96
107
|
try {
|
|
97
108
|
const ip = await fetch("https://api64.ipify.org").then((res) =>
|
|
98
109
|
res.text()
|
|
99
110
|
);
|
|
111
|
+
|
|
112
|
+
if (!ip) {
|
|
113
|
+
throw new Error("Could not fetch IP address");
|
|
114
|
+
}
|
|
115
|
+
|
|
100
116
|
const { country } = await maxMindClient.country(ip);
|
|
101
117
|
countryISOCode = country?.isoCode ?? "Unknown";
|
|
102
118
|
} catch (e) {
|
|
103
119
|
console.error("Geolocation failed:", e);
|
|
104
120
|
}
|
|
105
121
|
|
|
106
|
-
const
|
|
122
|
+
const analyticsEvent: analyticsEvent = {
|
|
107
123
|
...trackableEvent.data,
|
|
108
124
|
installationId,
|
|
109
125
|
countryISOCode,
|
|
110
126
|
};
|
|
111
127
|
|
|
112
|
-
// Forward to
|
|
128
|
+
// Forward to backend
|
|
113
129
|
const response = await fetch(`${platformUrl}/api/event`, {
|
|
114
130
|
keepalive: true,
|
|
115
131
|
method: "POST",
|
|
116
132
|
headers: {
|
|
117
133
|
"Content-Type": "application/json",
|
|
118
134
|
},
|
|
119
|
-
body: JSON.stringify(
|
|
135
|
+
body: JSON.stringify(analyticsEvent),
|
|
120
136
|
});
|
|
121
137
|
|
|
122
138
|
if (!response.ok) {
|
|
@@ -134,7 +150,7 @@ export const createRouteHandler = ({
|
|
|
134
150
|
error = `Analytics platform returned an internal server error. Please check the platform logs.`;
|
|
135
151
|
}
|
|
136
152
|
|
|
137
|
-
console.info(
|
|
153
|
+
console.info(`⚠️ Analytics platform rejected event: ${error}`);
|
|
138
154
|
return Response.json(
|
|
139
155
|
{
|
|
140
156
|
error,
|
|
@@ -156,22 +172,22 @@ export const createRouteHandler = ({
|
|
|
156
172
|
};
|
|
157
173
|
};
|
|
158
174
|
|
|
159
|
-
type ConsumerConfiguration = {
|
|
160
|
-
enabled?: boolean;
|
|
161
|
-
endpoint?: string;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
export type EventTrackerReturn = {
|
|
165
|
-
error: string | null;
|
|
166
|
-
success: boolean;
|
|
167
|
-
};
|
|
168
|
-
|
|
169
175
|
export const makeEventTracker =
|
|
170
|
-
({
|
|
171
|
-
|
|
172
|
-
|
|
176
|
+
({
|
|
177
|
+
enabled = false,
|
|
178
|
+
endpoint = "/api/analytics",
|
|
179
|
+
}: {
|
|
180
|
+
enabled?: boolean;
|
|
181
|
+
endpoint?: string;
|
|
182
|
+
}) =>
|
|
183
|
+
async (
|
|
184
|
+
event: RawEvent
|
|
185
|
+
): Promise<{
|
|
186
|
+
error: string | null;
|
|
187
|
+
success: boolean;
|
|
188
|
+
}> => {
|
|
173
189
|
if (!enabled) {
|
|
174
|
-
console.log("Analytics disabled
|
|
190
|
+
console.log("Analytics disabled - event not sent.");
|
|
175
191
|
return { error: null, success: true };
|
|
176
192
|
}
|
|
177
193
|
|
package/.turbo/turbo-test.log
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @codaco/analytics@0.6.0-alpha test /Users/jmh629/Projects/error-analytics-microservice/packages/analytics
|
|
3
|
-
> NODE_OPTIONS=--experimental-vm-modules jest "--watch"
|
|
4
|
-
|
|
5
|
-
(node:55883) ExperimentalWarning: VM Modules is an experimental feature and might change at any time
|
|
6
|
-
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
7
|
-
FAIL src/index.test.mjs
|
|
8
|
-
Analytics client package
|
|
9
|
-
✕ should be able to create a new client (1 ms)
|
|
10
|
-
|
|
11
|
-
● Analytics client package › should be able to create a new client
|
|
12
|
-
|
|
13
|
-
TypeError: Cannot read properties of undefined (reading 'platformUrl')
|
|
14
|
-
|
|
15
|
-
50 | private platformUrl?: string = "https://analytics.networkcanvas.dev";
|
|
16
|
-
51 | private installationId: string | null = null;
|
|
17
|
-
> 52 |
|
|
18
|
-
| ^
|
|
19
|
-
53 | private dispatchQueue: QueueObject<AnalyticsEventOrError>;
|
|
20
|
-
54 |
|
|
21
|
-
55 | private enabled: boolean = true;
|
|
22
|
-
|
|
23
|
-
at new s (src/index.ts:52:11)
|
|
24
|
-
at Object.<anonymous> (src/index.test.mjs:6:20)
|
|
25
|
-
|
|
26
|
-
Test Suites: 1 failed, 1 total
|
|
27
|
-
Tests: 1 failed, 1 total
|
|
28
|
-
Snapshots: 0 total
|
|
29
|
-
Time: 0.215 s, estimated 1 s
|
|
30
|
-
Ran all test suites related to changed files.
|
|
31
|
-
|
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["// 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 (value.isPrototypeOf(Error)) 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 {}\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 (typeof window !== \"undefined\")\n // browser should use relative path\n return \"\";\n\n if (process.env.VERCEL_URL)\n // reference for vercel.com\n return `https://${process.env.VERCEL_URL}`;\n\n if (process.env.NEXT_PUBLIC_URL)\n // Manually set deployment URL from env\n return process.env.NEXT_PUBLIC_URL;\n\n // assume localhost\n return `http://127.0.0.1:3000`;\n}\n","import { type NextRequest } from \"next/server\";\nimport { WebServiceClient } from \"@maxmind/geoip2-node\";\nimport { ensureError, getBaseUrl } 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\n// Properties that everything has in common.\nconst SharedEventAndErrorSchema = z.object({\n metadata: z.record(z.unknown()).optional(),\n});\n\nconst EventSchema = z.object({\n type: z.enum(eventTypes),\n});\n\nconst ErrorSchema = z.object({\n type: z.literal(\"Error\"),\n error: z\n .object({\n message: z.string(),\n name: z.string(),\n stack: z.string().optional(),\n })\n .strict(),\n});\n\n// Raw events are the events that are sent trackEvent.\nexport const RawEventSchema = z.discriminatedUnion(\"type\", [\n SharedEventAndErrorSchema.merge(EventSchema),\n SharedEventAndErrorSchema.merge(ErrorSchema),\n]);\nexport type RawEvent = z.infer<typeof RawEventSchema>;\n\n// Trackable events are the events that are sent to the route handler.\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// Dispatchable events are the events that are sent to the platform.\nconst DispatchablePropertiesSchema = z.object({\n installationId: z.string(),\n countryISOCode: z.string(),\n});\n\nexport const DispatchableEventSchema = z.intersection(\n TrackableEventSchema,\n DispatchablePropertiesSchema\n);\nexport type DispatchableEvent = z.infer<typeof DispatchableEventSchema>;\n\ntype RouteHandlerConfiguration = {\n platformUrl?: string;\n installationId: string;\n maxMindClient: WebServiceClient;\n};\n\nexport const createRouteHandler = ({\n platformUrl = \"https://analytics.networkcanvas.com\",\n installationId,\n maxMindClient,\n}: RouteHandlerConfiguration) => {\n return async (request: NextRequest) => {\n try {\n const incomingEvent = (await request.json()) as unknown;\n\n // Validate the event\n const trackableEvent = TrackableEventSchema.safeParse(incomingEvent);\n\n if (!trackableEvent.success) {\n console.error(\"Invalid event:\", trackableEvent.error);\n return new Response(JSON.stringify({ error: \"Invalid event\" }), {\n status: 400,\n headers: {\n \"Content-Type\": \"application/json\",\n },\n });\n }\n\n // We don't want failures in third party services to prevent us from\n // tracking analytics events.\n let countryISOCode = \"Unknown\";\n try {\n const ip = await fetch(\"https://api64.ipify.org\").then((res) =>\n res.text()\n );\n const { country } = await maxMindClient.country(ip);\n countryISOCode = country?.isoCode ?? \"Unknown\";\n } catch (e) {\n console.error(\"Geolocation failed:\", e);\n }\n\n const dispatchableEvent: DispatchableEvent = {\n ...trackableEvent.data,\n installationId,\n countryISOCode,\n };\n\n // Forward to microservice\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(dispatchableEvent),\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 console.info(\"⚠️ Analytics platform rejected event.\");\n return Response.json(\n {\n error,\n },\n { status: 500 }\n );\n }\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 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\ntype ConsumerConfiguration = {\n enabled?: boolean;\n endpoint?: string;\n};\n\nexport type EventTrackerReturn = {\n error: string | null;\n success: boolean;\n};\n\nexport const makeEventTracker =\n ({ enabled = false, endpoint = \"/api/analytics\" }: ConsumerConfiguration) =>\n async (event: RawEvent): Promise<EventTrackerReturn> => {\n // If analytics is disabled don't send analytics events.\n if (!enabled) {\n console.log(\"Analytics disabled, not sending event\");\n return { error: null, success: true };\n }\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"],"mappings":"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","names":[]}
|
package/jest.config.js
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* For a detailed explanation regarding each configuration property, visit:
|
|
3
|
-
* https://jestjs.io/docs/configuration
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/** @type {import('jest').Config} */
|
|
7
|
-
const config = {
|
|
8
|
-
// All imported modules in your tests should be mocked automatically
|
|
9
|
-
// automock: false,
|
|
10
|
-
|
|
11
|
-
// Stop running tests after `n` failures
|
|
12
|
-
// bail: 0,
|
|
13
|
-
|
|
14
|
-
// The directory where Jest should store its cached dependency information
|
|
15
|
-
// cacheDirectory: "/private/var/folders/vc/szx71k5j5sqcmrljl1w44ryddl7zmw/T/jest_pb0330",
|
|
16
|
-
|
|
17
|
-
// Automatically clear mock calls, instances, contexts and results before every test
|
|
18
|
-
// clearMocks: false,
|
|
19
|
-
|
|
20
|
-
// Indicates whether the coverage information should be collected while executing the test
|
|
21
|
-
// collectCoverage: false,
|
|
22
|
-
|
|
23
|
-
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
|
24
|
-
// collectCoverageFrom: undefined,
|
|
25
|
-
|
|
26
|
-
// The directory where Jest should output its coverage files
|
|
27
|
-
// coverageDirectory: undefined,
|
|
28
|
-
|
|
29
|
-
// An array of regexp pattern strings used to skip coverage collection
|
|
30
|
-
// coveragePathIgnorePatterns: [
|
|
31
|
-
// "/node_modules/"
|
|
32
|
-
// ],
|
|
33
|
-
|
|
34
|
-
// Indicates which provider should be used to instrument code for coverage
|
|
35
|
-
coverageProvider: "v8",
|
|
36
|
-
|
|
37
|
-
// A list of reporter names that Jest uses when writing coverage reports
|
|
38
|
-
// coverageReporters: [
|
|
39
|
-
// "json",
|
|
40
|
-
// "text",
|
|
41
|
-
// "lcov",
|
|
42
|
-
// "clover"
|
|
43
|
-
// ],
|
|
44
|
-
|
|
45
|
-
// An object that configures minimum threshold enforcement for coverage results
|
|
46
|
-
// coverageThreshold: undefined,
|
|
47
|
-
|
|
48
|
-
// A path to a custom dependency extractor
|
|
49
|
-
// dependencyExtractor: undefined,
|
|
50
|
-
|
|
51
|
-
// Make calling deprecated APIs throw helpful error messages
|
|
52
|
-
// errorOnDeprecated: false,
|
|
53
|
-
|
|
54
|
-
// The default configuration for fake timers
|
|
55
|
-
// fakeTimers: {
|
|
56
|
-
// "enableGlobally": false
|
|
57
|
-
// },
|
|
58
|
-
|
|
59
|
-
// Force coverage collection from ignored files using an array of glob patterns
|
|
60
|
-
// forceCoverageMatch: [],
|
|
61
|
-
|
|
62
|
-
// A path to a module which exports an async function that is triggered once before all test suites
|
|
63
|
-
// globalSetup: undefined,
|
|
64
|
-
|
|
65
|
-
// A path to a module which exports an async function that is triggered once after all test suites
|
|
66
|
-
// globalTeardown: undefined,
|
|
67
|
-
|
|
68
|
-
// A set of global variables that need to be available in all test environments
|
|
69
|
-
// globals: {},
|
|
70
|
-
|
|
71
|
-
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
|
72
|
-
// maxWorkers: "50%",
|
|
73
|
-
|
|
74
|
-
// An array of directory names to be searched recursively up from the requiring module's location
|
|
75
|
-
// moduleDirectories: [
|
|
76
|
-
// "node_modules"
|
|
77
|
-
// ],
|
|
78
|
-
|
|
79
|
-
// An array of file extensions your modules use
|
|
80
|
-
// moduleFileExtensions: [
|
|
81
|
-
// "js",
|
|
82
|
-
// "mjs",
|
|
83
|
-
// "cjs",
|
|
84
|
-
// "jsx",
|
|
85
|
-
// "ts",
|
|
86
|
-
// "tsx",
|
|
87
|
-
// "json",
|
|
88
|
-
// "node"
|
|
89
|
-
// ],
|
|
90
|
-
|
|
91
|
-
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
|
92
|
-
// moduleNameMapper: {},
|
|
93
|
-
|
|
94
|
-
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
|
95
|
-
// modulePathIgnorePatterns: [],
|
|
96
|
-
|
|
97
|
-
// Activates notifications for test results
|
|
98
|
-
// notify: false,
|
|
99
|
-
|
|
100
|
-
// An enum that specifies notification mode. Requires { notify: true }
|
|
101
|
-
// notifyMode: "failure-change",
|
|
102
|
-
|
|
103
|
-
// A preset that is used as a base for Jest's configuration
|
|
104
|
-
// preset: undefined,
|
|
105
|
-
|
|
106
|
-
// Run tests from one or more projects
|
|
107
|
-
// projects: undefined,
|
|
108
|
-
|
|
109
|
-
// Use this configuration option to add custom reporters to Jest
|
|
110
|
-
// reporters: undefined,
|
|
111
|
-
|
|
112
|
-
// Automatically reset mock state before every test
|
|
113
|
-
// resetMocks: false,
|
|
114
|
-
|
|
115
|
-
// Reset the module registry before running each individual test
|
|
116
|
-
// resetModules: false,
|
|
117
|
-
|
|
118
|
-
// A path to a custom resolver
|
|
119
|
-
// resolver: undefined,
|
|
120
|
-
|
|
121
|
-
// Automatically restore mock state and implementation before every test
|
|
122
|
-
// restoreMocks: false,
|
|
123
|
-
|
|
124
|
-
// The root directory that Jest should scan for tests and modules within
|
|
125
|
-
// rootDir: undefined,
|
|
126
|
-
|
|
127
|
-
// A list of paths to directories that Jest should use to search for files in
|
|
128
|
-
// roots: [
|
|
129
|
-
// "<rootDir>"
|
|
130
|
-
// ],
|
|
131
|
-
|
|
132
|
-
// Allows you to use a custom runner instead of Jest's default test runner
|
|
133
|
-
// runner: "jest-runner",
|
|
134
|
-
|
|
135
|
-
// The paths to modules that run some code to configure or set up the testing environment before each test
|
|
136
|
-
// setupFiles: [],
|
|
137
|
-
|
|
138
|
-
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
|
139
|
-
// setupFilesAfterEnv: [],
|
|
140
|
-
|
|
141
|
-
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
|
142
|
-
// slowTestThreshold: 5,
|
|
143
|
-
|
|
144
|
-
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
|
145
|
-
// snapshotSerializers: [],
|
|
146
|
-
|
|
147
|
-
// The test environment that will be used for testing
|
|
148
|
-
testEnvironment: "node",
|
|
149
|
-
|
|
150
|
-
// Options that will be passed to the testEnvironment
|
|
151
|
-
// testEnvironmentOptions: {},
|
|
152
|
-
|
|
153
|
-
// Adds a location field to test results
|
|
154
|
-
// testLocationInResults: false,
|
|
155
|
-
|
|
156
|
-
// The glob patterns Jest uses to detect test files
|
|
157
|
-
testMatch: [
|
|
158
|
-
"**/__tests__/**/*.[jt]s?(x)",
|
|
159
|
-
"**/?(*.)+(spec|test).m[tj]s?(x)"
|
|
160
|
-
],
|
|
161
|
-
|
|
162
|
-
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
|
163
|
-
// testPathIgnorePatterns: [
|
|
164
|
-
// "/node_modules/"
|
|
165
|
-
// ],
|
|
166
|
-
|
|
167
|
-
// The regexp pattern or array of patterns that Jest uses to detect test files
|
|
168
|
-
// testRegex: [],
|
|
169
|
-
|
|
170
|
-
// This option allows the use of a custom results processor
|
|
171
|
-
// testResultsProcessor: undefined,
|
|
172
|
-
|
|
173
|
-
// This option allows use of a custom test runner
|
|
174
|
-
// testRunner: "jest-circus/runner",
|
|
175
|
-
|
|
176
|
-
// A map from regular expressions to paths to transformers
|
|
177
|
-
// transform: undefined,
|
|
178
|
-
|
|
179
|
-
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
|
180
|
-
// transformIgnorePatterns: [
|
|
181
|
-
// "/node_modules/",
|
|
182
|
-
// "\\.pnp\\.[^\\/]+$"
|
|
183
|
-
// ],
|
|
184
|
-
|
|
185
|
-
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
|
186
|
-
// unmockedModulePathPatterns: undefined,
|
|
187
|
-
|
|
188
|
-
// Indicates whether each individual test should be reported during the run
|
|
189
|
-
// verbose: undefined,
|
|
190
|
-
|
|
191
|
-
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
|
192
|
-
// watchPathIgnorePatterns: [],
|
|
193
|
-
|
|
194
|
-
// Whether to use watchman for file crawling
|
|
195
|
-
// watchman: true,
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
module.exports = config;
|