@codaco/analytics 2.1.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +54 -21
- package/dist/index.mjs +24 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +26 -24
- package/src/index.ts +39 -42
package/dist/index.d.mts
CHANGED
|
@@ -1,32 +1,65 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server';
|
|
2
2
|
import { WebServiceClient } from '@maxmind/geoip2-node';
|
|
3
|
+
import z from 'zod';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type: "
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
declare const eventTypes: readonly ["AppSetup", "ProtocolInstalled", "InterviewStarted", "InterviewCompleted", "DataExported", "Error"];
|
|
6
|
+
type EventType = (typeof eventTypes)[number];
|
|
7
|
+
type EventTypeWithoutError = Exclude<EventType, "Error">;
|
|
8
|
+
declare const EventsSchema: z.ZodObject<{
|
|
9
|
+
type: z.ZodEnum<["AppSetup", "ProtocolInstalled", "InterviewStarted", "InterviewCompleted", "DataExported", "Error"]>;
|
|
10
|
+
installationId: z.ZodString;
|
|
11
|
+
timestamp: z.ZodString;
|
|
12
|
+
isocode: z.ZodOptional<z.ZodString>;
|
|
13
|
+
error: z.ZodOptional<z.ZodObject<{
|
|
14
|
+
message: z.ZodString;
|
|
15
|
+
name: z.ZodString;
|
|
16
|
+
stack: z.ZodOptional<z.ZodString>;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
message: string;
|
|
19
|
+
name: string;
|
|
20
|
+
stack?: string | undefined;
|
|
21
|
+
}, {
|
|
22
|
+
message: string;
|
|
23
|
+
name: string;
|
|
24
|
+
stack?: string | undefined;
|
|
25
|
+
}>>;
|
|
26
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
27
|
+
}, "strip", z.ZodTypeAny, {
|
|
28
|
+
type: "AppSetup" | "ProtocolInstalled" | "InterviewStarted" | "InterviewCompleted" | "DataExported" | "Error";
|
|
29
|
+
installationId: string;
|
|
30
|
+
timestamp: string;
|
|
31
|
+
isocode?: string | undefined;
|
|
32
|
+
error?: {
|
|
33
|
+
message: string;
|
|
34
|
+
name: string;
|
|
35
|
+
stack?: string | undefined;
|
|
36
|
+
} | undefined;
|
|
37
|
+
metadata?: Record<string, unknown> | undefined;
|
|
38
|
+
}, {
|
|
39
|
+
type: "AppSetup" | "ProtocolInstalled" | "InterviewStarted" | "InterviewCompleted" | "DataExported" | "Error";
|
|
40
|
+
installationId: string;
|
|
41
|
+
timestamp: string;
|
|
42
|
+
isocode?: string | undefined;
|
|
43
|
+
error?: {
|
|
44
|
+
message: string;
|
|
45
|
+
name: string;
|
|
46
|
+
stack?: string | undefined;
|
|
47
|
+
} | undefined;
|
|
48
|
+
metadata?: Record<string, unknown> | undefined;
|
|
49
|
+
}>;
|
|
50
|
+
type Event = z.infer<typeof EventsSchema>;
|
|
51
|
+
type AnalyticsEvent = {
|
|
52
|
+
type: EventTypeWithoutError;
|
|
12
53
|
metadata?: Record<string, unknown>;
|
|
13
54
|
};
|
|
14
|
-
type AnalyticsError =
|
|
55
|
+
type AnalyticsError = {
|
|
15
56
|
type: "Error";
|
|
16
|
-
error:
|
|
17
|
-
|
|
18
|
-
details: string;
|
|
19
|
-
stacktrace: string;
|
|
20
|
-
path: string;
|
|
21
|
-
};
|
|
57
|
+
error: Error;
|
|
58
|
+
metadata?: Record<string, unknown>;
|
|
22
59
|
};
|
|
23
60
|
type AnalyticsEventOrError = AnalyticsEvent | AnalyticsError;
|
|
24
61
|
type AnalyticsEventOrErrorWithTimestamp = AnalyticsEventOrError & {
|
|
25
|
-
timestamp:
|
|
26
|
-
};
|
|
27
|
-
type DispatchableAnalyticsEvent = AnalyticsEventOrErrorWithTimestamp & {
|
|
28
|
-
installationId: string;
|
|
29
|
-
geolocation?: GeoLocation;
|
|
62
|
+
timestamp: string;
|
|
30
63
|
};
|
|
31
64
|
type RouteHandlerConfiguration = {
|
|
32
65
|
platformUrl?: string;
|
|
@@ -36,4 +69,4 @@ type RouteHandlerConfiguration = {
|
|
|
36
69
|
declare const createRouteHandler: ({ platformUrl, installationId, maxMindClient, }: RouteHandlerConfiguration) => (request: NextRequest) => Promise<Response>;
|
|
37
70
|
declare const makeEventTracker: (endpoint?: string) => (event: AnalyticsEventOrError) => Promise<void>;
|
|
38
71
|
|
|
39
|
-
export { AnalyticsError, AnalyticsEvent,
|
|
72
|
+
export { AnalyticsError, AnalyticsEvent, AnalyticsEventOrError, AnalyticsEventOrErrorWithTimestamp, Event, EventType, EventsSchema, createRouteHandler, eventTypes, makeEventTracker };
|
package/dist/index.mjs
CHANGED
|
@@ -27,6 +27,27 @@ function getBaseUrl() {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// src/index.ts
|
|
30
|
+
import z from "zod";
|
|
31
|
+
var eventTypes = [
|
|
32
|
+
"AppSetup",
|
|
33
|
+
"ProtocolInstalled",
|
|
34
|
+
"InterviewStarted",
|
|
35
|
+
"InterviewCompleted",
|
|
36
|
+
"DataExported",
|
|
37
|
+
"Error"
|
|
38
|
+
];
|
|
39
|
+
var EventsSchema = z.object({
|
|
40
|
+
type: z.enum(eventTypes),
|
|
41
|
+
installationId: z.string(),
|
|
42
|
+
timestamp: z.string(),
|
|
43
|
+
isocode: z.string().optional(),
|
|
44
|
+
error: z.object({
|
|
45
|
+
message: z.string(),
|
|
46
|
+
name: z.string(),
|
|
47
|
+
stack: z.string().optional()
|
|
48
|
+
}).optional(),
|
|
49
|
+
metadata: z.record(z.unknown()).optional()
|
|
50
|
+
});
|
|
30
51
|
var createRouteHandler = ({
|
|
31
52
|
platformUrl = "https://analytics.networkcanvas.com",
|
|
32
53
|
installationId,
|
|
@@ -43,9 +64,7 @@ var createRouteHandler = ({
|
|
|
43
64
|
const dispatchableEvent = {
|
|
44
65
|
...event,
|
|
45
66
|
installationId,
|
|
46
|
-
|
|
47
|
-
countryCode
|
|
48
|
-
}
|
|
67
|
+
isocode: countryCode
|
|
49
68
|
};
|
|
50
69
|
const response = await fetch(`${platformUrl}/api/event`, {
|
|
51
70
|
keepalive: true,
|
|
@@ -79,8 +98,6 @@ var createRouteHandler = ({
|
|
|
79
98
|
}
|
|
80
99
|
);
|
|
81
100
|
}
|
|
82
|
-
console.info(`\u{1F680} Analytics event forwarded successfully.`);
|
|
83
|
-
console.info(JSON.stringify(dispatchableEvent, null, 2));
|
|
84
101
|
return new Response(
|
|
85
102
|
JSON.stringify({ message: "Event forwarded successfully" }),
|
|
86
103
|
{
|
|
@@ -140,7 +157,9 @@ var makeEventTracker = (endpoint = "/api/analytics") => async (event) => {
|
|
|
140
157
|
}
|
|
141
158
|
};
|
|
142
159
|
export {
|
|
160
|
+
EventsSchema,
|
|
143
161
|
createRouteHandler,
|
|
162
|
+
eventTypes,
|
|
144
163
|
makeEventTracker
|
|
145
164
|
};
|
|
146
165
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["// Helper function that ensures that a value is an Error\r\nexport function ensureError(value: unknown): Error {\r\n if (!value) return new Error(\"No value was thrown\");\r\n\r\n if (value instanceof Error) return value;\r\n\r\n // Test if value inherits from Error\r\n if (value.isPrototypeOf(Error)) return value as Error & typeof value;\r\n\r\n let stringified = \"[Unable to stringify the thrown value]\";\r\n try {\r\n stringified = JSON.stringify(value);\r\n } catch {}\r\n\r\n const error = new Error(\r\n `This value was thrown as is, not through an Error: ${stringified}`\r\n );\r\n return error;\r\n}\r\n\r\nexport function getBaseUrl() {\r\n if (typeof window !== \"undefined\")\r\n // browser should use relative path\r\n return \"\";\r\n\r\n if (process.env.VERCEL_URL)\r\n // reference for vercel.com\r\n return `https://${process.env.VERCEL_URL}`;\r\n\r\n if (process.env.NEXT_PUBLIC_URL)\r\n // Manually set deployment URL from env\r\n return process.env.NEXT_PUBLIC_URL;\r\n\r\n // assume localhost\r\n return `http://127.0.0.1:3000`;\r\n}\r\n","import type { NextRequest } from \"next/server\";\r\nimport { WebServiceClient } from \"@maxmind/geoip2-node\";\r\nimport { ensureError, getBaseUrl } from \"./utils\";\r\
|
|
1
|
+
{"version":3,"sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["// Helper function that ensures that a value is an Error\r\nexport function ensureError(value: unknown): Error {\r\n if (!value) return new Error(\"No value was thrown\");\r\n\r\n if (value instanceof Error) return value;\r\n\r\n // Test if value inherits from Error\r\n if (value.isPrototypeOf(Error)) return value as Error & typeof value;\r\n\r\n let stringified = \"[Unable to stringify the thrown value]\";\r\n try {\r\n stringified = JSON.stringify(value);\r\n } catch {}\r\n\r\n const error = new Error(\r\n `This value was thrown as is, not through an Error: ${stringified}`\r\n );\r\n return error;\r\n}\r\n\r\nexport function getBaseUrl() {\r\n if (typeof window !== \"undefined\")\r\n // browser should use relative path\r\n return \"\";\r\n\r\n if (process.env.VERCEL_URL)\r\n // reference for vercel.com\r\n return `https://${process.env.VERCEL_URL}`;\r\n\r\n if (process.env.NEXT_PUBLIC_URL)\r\n // Manually set deployment URL from env\r\n return process.env.NEXT_PUBLIC_URL;\r\n\r\n // assume localhost\r\n return `http://127.0.0.1:3000`;\r\n}\r\n","import type { NextRequest } from \"next/server\";\r\nimport { WebServiceClient } from \"@maxmind/geoip2-node\";\r\nimport { ensureError, getBaseUrl } from \"./utils\";\r\nimport z from \"zod\";\r\n\r\nexport const eventTypes = [\r\n \"AppSetup\",\r\n \"ProtocolInstalled\",\r\n \"InterviewStarted\",\r\n \"InterviewCompleted\",\r\n \"DataExported\",\r\n \"Error\",\r\n] as const;\r\n\r\nexport type EventType = (typeof eventTypes)[number];\r\ntype EventTypeWithoutError = Exclude<EventType, \"Error\">;\r\n\r\nexport const EventsSchema = z.object({\r\n type: z.enum(eventTypes),\r\n installationId: z.string(),\r\n timestamp: z.string(),\r\n isocode: z.string().optional(),\r\n error: z\r\n .object({\r\n message: z.string(),\r\n name: z.string(),\r\n stack: z.string().optional(),\r\n })\r\n .optional(),\r\n metadata: z.record(z.unknown()).optional(),\r\n});\r\n\r\nexport type Event = z.infer<typeof EventsSchema>;\r\n\r\nexport type AnalyticsEvent = {\r\n type: EventTypeWithoutError;\r\n metadata?: Record<string, unknown>;\r\n};\r\n\r\nexport type AnalyticsError = {\r\n type: \"Error\";\r\n error: Error;\r\n metadata?: Record<string, unknown>;\r\n};\r\n\r\nexport type AnalyticsEventOrError = AnalyticsEvent | AnalyticsError;\r\n\r\nexport type AnalyticsEventOrErrorWithTimestamp = AnalyticsEventOrError & {\r\n timestamp: string;\r\n};\r\n\r\ntype RouteHandlerConfiguration = {\r\n platformUrl?: string;\r\n installationId: string;\r\n maxMindClient: WebServiceClient;\r\n};\r\n\r\nexport const createRouteHandler = ({\r\n platformUrl = \"https://analytics.networkcanvas.com\",\r\n installationId,\r\n maxMindClient,\r\n}: RouteHandlerConfiguration) => {\r\n return async (request: NextRequest) => {\r\n try {\r\n const event =\r\n (await request.json()) as AnalyticsEventOrErrorWithTimestamp;\r\n\r\n const ip = await fetch(\"https://api64.ipify.org\").then((res) =>\r\n res.text()\r\n );\r\n\r\n const { country } = await maxMindClient.country(ip);\r\n const countryCode = country?.isoCode ?? \"Unknown\";\r\n\r\n const dispatchableEvent: Event = {\r\n ...event,\r\n installationId,\r\n isocode: countryCode,\r\n };\r\n\r\n // Forward to microservice\r\n const response = await fetch(`${platformUrl}/api/event`, {\r\n keepalive: true,\r\n method: \"POST\",\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n },\r\n body: JSON.stringify(dispatchableEvent),\r\n });\r\n\r\n if (!response.ok) {\r\n if (response.status === 404) {\r\n console.error(\r\n `Analytics platform not found. Please specify a valid platform URL.`\r\n );\r\n } else if (response.status === 500) {\r\n console.error(\r\n `Internal server error on analytics platform when forwarding event: ${response.statusText}.`\r\n );\r\n } else {\r\n console.error(\r\n `General error when forwarding event: ${response.statusText}`\r\n );\r\n }\r\n\r\n return new Response(\r\n JSON.stringify({ error: \"Internal Server Error\" }),\r\n {\r\n status: 500,\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n },\r\n }\r\n );\r\n }\r\n\r\n return new Response(\r\n JSON.stringify({ message: \"Event forwarded successfully\" }),\r\n {\r\n status: 200,\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n },\r\n }\r\n );\r\n } catch (e) {\r\n const error = ensureError(e);\r\n console.error(\"Error in route handler:\", error);\r\n\r\n // Return an appropriate error response\r\n return new Response(JSON.stringify({ error: \"Internal Server Error\" }), {\r\n status: 500,\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n },\r\n });\r\n }\r\n };\r\n};\r\n\r\nexport const makeEventTracker =\r\n (endpoint: string = \"/api/analytics\") =>\r\n async (event: AnalyticsEventOrError) => {\r\n const endpointWithHost = getBaseUrl() + endpoint;\r\n\r\n const eventWithTimeStamp = {\r\n ...event,\r\n timestamp: new Date(),\r\n };\r\n\r\n try {\r\n const response = await fetch(endpointWithHost, {\r\n method: \"POST\",\r\n keepalive: true,\r\n body: JSON.stringify(eventWithTimeStamp),\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n },\r\n });\r\n\r\n if (!response.ok) {\r\n if (response.status === 404) {\r\n console.error(\r\n `Analytics endpoint not found, did you forget to add the route?`\r\n );\r\n return;\r\n }\r\n\r\n if (response.status === 500) {\r\n console.error(\r\n `Internal server error when sending analytics event: ${response.statusText}. Check the route handler implementation.`\r\n );\r\n return;\r\n }\r\n\r\n console.error(\r\n `General error sending analytics event: ${response.statusText}`\r\n );\r\n }\r\n } catch (e) {\r\n const error = ensureError(e);\r\n\r\n console.error(\"Internal error with analytics:\", error.message);\r\n }\r\n };\r\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@codaco/analytics",
|
|
3
|
-
"version": "
|
|
4
|
-
"module": "./dist/index.mjs",
|
|
5
|
-
"types": "./dist/index.d.mts",
|
|
6
|
-
"author": "Complex Data Collective <developers@coda.co>",
|
|
7
|
-
"description": "Utilities for tracking analytics and error reporting in Fresco",
|
|
8
|
-
"scripts": {
|
|
9
|
-
"build": "tsup src/index.ts --format esm --dts --clean --sourcemap",
|
|
10
|
-
"lint": "eslint .",
|
|
11
|
-
"dev": "npm run build -- --watch"
|
|
12
|
-
},
|
|
13
|
-
"peerDependencies": {
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
},
|
|
17
|
-
"devDependencies": {
|
|
18
|
-
"eslint-config-custom": "workspace:*",
|
|
19
|
-
"tsconfig": "workspace:*",
|
|
20
|
-
"tsup": "^7.2.0",
|
|
21
|
-
"typescript": "^5.3.2"
|
|
22
|
-
},
|
|
23
|
-
"dependencies": {
|
|
24
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@codaco/analytics",
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"module": "./dist/index.mjs",
|
|
5
|
+
"types": "./dist/index.d.mts",
|
|
6
|
+
"author": "Complex Data Collective <developers@coda.co>",
|
|
7
|
+
"description": "Utilities for tracking analytics and error reporting in Fresco",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsup src/index.ts --format esm --dts --clean --sourcemap",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"dev": "npm run build -- --watch"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"@maxmind/geoip2-node": "^5.0.0",
|
|
15
|
+
"next": "13 || 14"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"eslint-config-custom": "workspace:*",
|
|
19
|
+
"tsconfig": "workspace:*",
|
|
20
|
+
"tsup": "^7.2.0",
|
|
21
|
+
"typescript": "^5.3.2"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"zod": "^3.22.4"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,50 +1,52 @@
|
|
|
1
1
|
import type { NextRequest } from "next/server";
|
|
2
2
|
import { WebServiceClient } from "@maxmind/geoip2-node";
|
|
3
3
|
import { ensureError, getBaseUrl } from "./utils";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
4
|
+
import z from "zod";
|
|
5
|
+
|
|
6
|
+
export const eventTypes = [
|
|
7
|
+
"AppSetup",
|
|
8
|
+
"ProtocolInstalled",
|
|
9
|
+
"InterviewStarted",
|
|
10
|
+
"InterviewCompleted",
|
|
11
|
+
"DataExported",
|
|
12
|
+
"Error",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export type EventType = (typeof eventTypes)[number];
|
|
16
|
+
type EventTypeWithoutError = Exclude<EventType, "Error">;
|
|
17
|
+
|
|
18
|
+
export const EventsSchema = z.object({
|
|
19
|
+
type: z.enum(eventTypes),
|
|
20
|
+
installationId: z.string(),
|
|
21
|
+
timestamp: z.string(),
|
|
22
|
+
isocode: z.string().optional(),
|
|
23
|
+
error: z
|
|
24
|
+
.object({
|
|
25
|
+
message: z.string(),
|
|
26
|
+
name: z.string(),
|
|
27
|
+
stack: z.string().optional(),
|
|
28
|
+
})
|
|
29
|
+
.optional(),
|
|
30
|
+
metadata: z.record(z.unknown()).optional(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export type Event = z.infer<typeof EventsSchema>;
|
|
34
|
+
|
|
35
|
+
export type AnalyticsEvent = {
|
|
36
|
+
type: EventTypeWithoutError;
|
|
26
37
|
metadata?: Record<string, unknown>;
|
|
27
38
|
};
|
|
28
39
|
|
|
29
|
-
export type AnalyticsError =
|
|
40
|
+
export type AnalyticsError = {
|
|
30
41
|
type: "Error";
|
|
31
|
-
error:
|
|
32
|
-
|
|
33
|
-
details: string;
|
|
34
|
-
stacktrace: string;
|
|
35
|
-
path: string;
|
|
36
|
-
};
|
|
42
|
+
error: Error;
|
|
43
|
+
metadata?: Record<string, unknown>;
|
|
37
44
|
};
|
|
38
45
|
|
|
39
46
|
export type AnalyticsEventOrError = AnalyticsEvent | AnalyticsError;
|
|
40
47
|
|
|
41
48
|
export type AnalyticsEventOrErrorWithTimestamp = AnalyticsEventOrError & {
|
|
42
|
-
timestamp:
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export type DispatchableAnalyticsEvent = AnalyticsEventOrErrorWithTimestamp & {
|
|
46
|
-
installationId: string;
|
|
47
|
-
geolocation?: GeoLocation;
|
|
49
|
+
timestamp: string;
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
type RouteHandlerConfiguration = {
|
|
@@ -70,12 +72,10 @@ export const createRouteHandler = ({
|
|
|
70
72
|
const { country } = await maxMindClient.country(ip);
|
|
71
73
|
const countryCode = country?.isoCode ?? "Unknown";
|
|
72
74
|
|
|
73
|
-
const dispatchableEvent:
|
|
75
|
+
const dispatchableEvent: Event = {
|
|
74
76
|
...event,
|
|
75
77
|
installationId,
|
|
76
|
-
|
|
77
|
-
countryCode,
|
|
78
|
-
},
|
|
78
|
+
isocode: countryCode,
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
// Forward to microservice
|
|
@@ -114,9 +114,6 @@ export const createRouteHandler = ({
|
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
console.info(`🚀 Analytics event forwarded successfully.`);
|
|
118
|
-
console.info(JSON.stringify(dispatchableEvent, null, 2));
|
|
119
|
-
|
|
120
117
|
return new Response(
|
|
121
118
|
JSON.stringify({ message: "Event forwarded successfully" }),
|
|
122
119
|
{
|