@codaco/analytics 1.0.0-alpha-1 → 2.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 +6 -6
- package/dist/index.d.mts +6 -10
- package/dist/index.mjs +107 -44
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -5
- package/src/index.ts +120 -64
- package/src/utils.ts +17 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @codaco/analytics@
|
|
2
|
+
> @codaco/analytics@2.0.0 build /Users/jmh629/Projects/error-analytics-microservice/packages/analytics
|
|
3
3
|
> tsup src/index.ts --format esm --dts --clean --sourcemap
|
|
4
4
|
|
|
5
5
|
CLI Building entry: src/index.ts
|
|
@@ -8,9 +8,9 @@ CLI tsup v7.2.0
|
|
|
8
8
|
CLI Target: es2022
|
|
9
9
|
CLI Cleaning output folder
|
|
10
10
|
ESM Build start
|
|
11
|
-
ESM dist/index.mjs
|
|
12
|
-
ESM dist/index.mjs.map
|
|
13
|
-
ESM ⚡️ Build success in
|
|
11
|
+
ESM dist/index.mjs 4.06 KB
|
|
12
|
+
ESM dist/index.mjs.map 8.47 KB
|
|
13
|
+
ESM ⚡️ Build success in 123ms
|
|
14
14
|
DTS Build start
|
|
15
|
-
DTS ⚡️ Build success in
|
|
16
|
-
DTS dist/index.d.mts 1.
|
|
15
|
+
DTS ⚡️ Build success in 776ms
|
|
16
|
+
DTS dist/index.d.mts 1.48 KB
|
package/dist/index.d.mts
CHANGED
|
@@ -5,10 +5,10 @@ type GeoLocation = {
|
|
|
5
5
|
countryCode: string;
|
|
6
6
|
};
|
|
7
7
|
type AnalyticsEventBase = {
|
|
8
|
-
type: "InterviewCompleted" | "InterviewStarted" | "ProtocolInstalled" | "AppSetup" | "Error"
|
|
8
|
+
type: "InterviewCompleted" | "InterviewStarted" | "ProtocolInstalled" | "AppSetup" | "Error";
|
|
9
9
|
};
|
|
10
10
|
type AnalyticsEvent = AnalyticsEventBase & {
|
|
11
|
-
type: "InterviewCompleted" | "InterviewStarted" | "ProtocolInstalled" | "AppSetup"
|
|
11
|
+
type: "InterviewCompleted" | "InterviewStarted" | "ProtocolInstalled" | "AppSetup";
|
|
12
12
|
metadata?: Record<string, unknown>;
|
|
13
13
|
};
|
|
14
14
|
type AnalyticsError = AnalyticsEventBase & {
|
|
@@ -29,15 +29,11 @@ type DispatchableAnalyticsEvent = AnalyticsEventOrErrorWithTimestamp & {
|
|
|
29
29
|
geolocation?: GeoLocation;
|
|
30
30
|
};
|
|
31
31
|
type RouteHandlerConfiguration = {
|
|
32
|
-
maxMindAccountId: string;
|
|
33
|
-
maxMindLicenseKey: string;
|
|
34
32
|
platformUrl?: string;
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
installationId: string;
|
|
34
|
+
maxMindClient: WebServiceClient;
|
|
37
35
|
};
|
|
38
|
-
declare const createRouteHandler: ({
|
|
39
|
-
declare const makeEventTracker: (
|
|
40
|
-
endpoint: string;
|
|
41
|
-
}) => (event: AnalyticsEventOrError) => void;
|
|
36
|
+
declare const createRouteHandler: ({ platformUrl, installationId, maxMindClient, }: RouteHandlerConfiguration) => (request: NextRequest) => Promise<Response>;
|
|
37
|
+
declare const makeEventTracker: (endpoint?: string) => (event: AnalyticsEventOrError) => Promise<void>;
|
|
42
38
|
|
|
43
39
|
export { AnalyticsError, AnalyticsEvent, AnalyticsEventBase, AnalyticsEventOrError, AnalyticsEventOrErrorWithTimestamp, DispatchableAnalyticsEvent, createRouteHandler, makeEventTracker };
|
package/dist/index.mjs
CHANGED
|
@@ -16,65 +16,128 @@ function ensureError(value) {
|
|
|
16
16
|
);
|
|
17
17
|
return error;
|
|
18
18
|
}
|
|
19
|
+
function getBaseUrl() {
|
|
20
|
+
if (typeof window !== "undefined")
|
|
21
|
+
return "";
|
|
22
|
+
if (process.env.VERCEL_URL)
|
|
23
|
+
return `https://${process.env.VERCEL_URL}`;
|
|
24
|
+
if (process.env.NEXT_PUBLIC_URL)
|
|
25
|
+
return process.env.NEXT_PUBLIC_URL;
|
|
26
|
+
return `http://127.0.0.1:3000`;
|
|
27
|
+
}
|
|
19
28
|
|
|
20
29
|
// src/index.ts
|
|
21
30
|
var createRouteHandler = ({
|
|
22
|
-
maxMindAccountId,
|
|
23
|
-
maxMindLicenseKey,
|
|
24
31
|
platformUrl = "https://analytics.networkcanvas.com",
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
installationId,
|
|
33
|
+
maxMindClient
|
|
27
34
|
}) => {
|
|
28
35
|
return async (request) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
try {
|
|
37
|
+
const event = await request.json();
|
|
38
|
+
const ip = await fetch("https://api64.ipify.org").then(
|
|
39
|
+
(res) => res.text()
|
|
40
|
+
);
|
|
41
|
+
const { country } = await maxMindClient.country(ip);
|
|
42
|
+
const countryCode = country?.isoCode ?? "Unknown";
|
|
43
|
+
const dispatchableEvent = {
|
|
44
|
+
...event,
|
|
45
|
+
installationId,
|
|
46
|
+
geolocation: {
|
|
47
|
+
countryCode
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const response = await fetch(`${platformUrl}/api/event`, {
|
|
51
|
+
keepalive: true,
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
"Content-Type": "application/json"
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify(dispatchableEvent)
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
if (response.status === 404) {
|
|
60
|
+
console.error(
|
|
61
|
+
`Analytics platform not found. Please specify a valid platform URL.`
|
|
62
|
+
);
|
|
63
|
+
} else if (response.status === 500) {
|
|
64
|
+
console.error(
|
|
65
|
+
`Internal server error on analytics platform when forwarding event: ${response.statusText}.`
|
|
66
|
+
);
|
|
67
|
+
} else {
|
|
68
|
+
console.error(
|
|
69
|
+
`General error when forwarding event: ${response.statusText}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return new Response(
|
|
73
|
+
JSON.stringify({ error: "Internal Server Error" }),
|
|
74
|
+
{
|
|
75
|
+
status: 500,
|
|
76
|
+
headers: {
|
|
77
|
+
"Content-Type": "application/json"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
);
|
|
46
81
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
console.info(`\u{1F680} Analytics event forwarded successfully.`);
|
|
83
|
+
console.info(JSON.stringify(dispatchableEvent, null, 2));
|
|
84
|
+
return new Response(
|
|
85
|
+
JSON.stringify({ message: "Event forwarded successfully" }),
|
|
86
|
+
{
|
|
87
|
+
status: 200,
|
|
88
|
+
headers: {
|
|
89
|
+
"Content-Type": "application/json"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
const error = ensureError(e);
|
|
95
|
+
console.error("Error in route handler:", error);
|
|
96
|
+
return new Response(JSON.stringify({ error: "Internal Server Error" }), {
|
|
97
|
+
status: 500,
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "application/json"
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
60
103
|
};
|
|
61
104
|
};
|
|
62
|
-
var makeEventTracker = (
|
|
105
|
+
var makeEventTracker = (endpoint = "/api/analytics") => async (event) => {
|
|
106
|
+
const endpointWithHost = getBaseUrl() + endpoint;
|
|
63
107
|
const eventWithTimeStamp = {
|
|
64
108
|
...event,
|
|
65
109
|
timestamp: /* @__PURE__ */ new Date()
|
|
66
110
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch(endpointWithHost, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
keepalive: true,
|
|
115
|
+
body: JSON.stringify(eventWithTimeStamp),
|
|
116
|
+
headers: {
|
|
117
|
+
"Content-Type": "application/json"
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
if (response.status === 404) {
|
|
122
|
+
console.error(
|
|
123
|
+
`Analytics endpoint not found, did you forget to add the route?`
|
|
124
|
+
);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (response.status === 500) {
|
|
128
|
+
console.error(
|
|
129
|
+
`Internal server error when sending analytics event: ${response.statusText}. Check the route handler implementation.`
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
console.error(
|
|
134
|
+
`General error sending analytics event: ${response.statusText}`
|
|
135
|
+
);
|
|
73
136
|
}
|
|
74
|
-
}
|
|
137
|
+
} catch (e) {
|
|
75
138
|
const error = ensureError(e);
|
|
76
|
-
console.error("
|
|
77
|
-
}
|
|
139
|
+
console.error("Internal error with analytics:", error.message);
|
|
140
|
+
}
|
|
78
141
|
};
|
|
79
142
|
export {
|
|
80
143
|
createRouteHandler,
|
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\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","import type { NextRequest } from \"next/server\";\nimport { WebServiceClient } from \"@maxmind/geoip2-node\";\nimport { ensureError } from \"./utils\";\n\ntype GeoLocation = {\n countryCode: string;\n};\n\nexport type AnalyticsEventBase = {\n type:\n | \"InterviewCompleted\"\n | \"InterviewStarted\"\n | \"ProtocolInstalled\"\n | \"AppSetup\"\n | \"Error\"
|
|
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\";\n\ntype GeoLocation = {\n countryCode: string;\n};\n\nexport type AnalyticsEventBase = {\n type:\n | \"InterviewCompleted\"\n | \"InterviewStarted\"\n | \"ProtocolInstalled\"\n | \"AppSetup\"\n | \"Error\";\n};\n\nexport type AnalyticsEvent = AnalyticsEventBase & {\n type:\n | \"InterviewCompleted\"\n | \"InterviewStarted\"\n | \"ProtocolInstalled\"\n | \"AppSetup\";\n metadata?: Record<string, unknown>;\n};\n\nexport type AnalyticsError = AnalyticsEventBase & {\n type: \"Error\";\n error: {\n message: string;\n details: string;\n stacktrace: string;\n path: string;\n };\n};\n\nexport type AnalyticsEventOrError = AnalyticsEvent | AnalyticsError;\n\nexport type AnalyticsEventOrErrorWithTimestamp = AnalyticsEventOrError & {\n timestamp: Date;\n};\n\nexport type DispatchableAnalyticsEvent = AnalyticsEventOrErrorWithTimestamp & {\n installationId: string;\n geolocation?: GeoLocation;\n};\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 event =\n (await request.json()) as AnalyticsEventOrErrorWithTimestamp;\n\n const ip = await fetch(\"https://api64.ipify.org\").then((res) =>\n res.text()\n );\n\n const { country } = await maxMindClient.country(ip);\n const countryCode = country?.isoCode ?? \"Unknown\";\n\n const dispatchableEvent: DispatchableAnalyticsEvent = {\n ...event,\n installationId,\n geolocation: {\n countryCode,\n },\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 if (response.status === 404) {\n console.error(\n `Analytics platform not found. Please specify a valid platform URL.`\n );\n } else if (response.status === 500) {\n console.error(\n `Internal server error on analytics platform when forwarding event: ${response.statusText}.`\n );\n } else {\n console.error(\n `General error when forwarding event: ${response.statusText}`\n );\n }\n\n return new Response(\n JSON.stringify({ error: \"Internal Server Error\" }),\n {\n status: 500,\n headers: {\n \"Content-Type\": \"application/json\",\n },\n }\n );\n }\n\n console.info(`🚀 Analytics event forwarded successfully.`);\n console.info(JSON.stringify(dispatchableEvent, null, 2));\n\n return new Response(\n JSON.stringify({ message: \"Event forwarded successfully\" }),\n {\n status: 200,\n headers: {\n \"Content-Type\": \"application/json\",\n },\n }\n );\n } catch (e) {\n const error = ensureError(e);\n console.error(\"Error in route handler:\", error);\n\n // Return an appropriate error response\n return new Response(JSON.stringify({ error: \"Internal Server Error\" }), {\n status: 500,\n headers: {\n \"Content-Type\": \"application/json\",\n },\n });\n }\n };\n};\n\nexport const makeEventTracker =\n (endpoint: string = \"/api/analytics\") =>\n async (event: AnalyticsEventOrError) => {\n const endpointWithHost = getBaseUrl() + endpoint;\n\n const eventWithTimeStamp = {\n ...event,\n timestamp: new Date(),\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 console.error(\n `Analytics endpoint not found, did you forget to add the route?`\n );\n return;\n }\n\n if (response.status === 500) {\n console.error(\n `Internal server error when sending analytics event: ${response.statusText}. Check the route handler implementation.`\n );\n return;\n }\n\n console.error(\n `General error sending analytics event: ${response.statusText}`\n );\n }\n } catch (e) {\n const error = ensureError(e);\n\n console.error(\"Internal error with analytics:\", error.message);\n }\n };\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codaco/analytics",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"module": "./dist/index.mjs",
|
|
5
5
|
"types": "./dist/index.d.mts",
|
|
6
6
|
"author": "Complex Data Collective <developers@coda.co>",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"dev": "npm run build -- --watch"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"next": "13 || 14"
|
|
14
|
+
"next": "13 || 14",
|
|
15
|
+
"@maxmind/geoip2-node": "^5.0.0"
|
|
15
16
|
},
|
|
16
17
|
"devDependencies": {
|
|
17
18
|
"eslint-config-custom": "workspace:*",
|
|
@@ -19,7 +20,5 @@
|
|
|
19
20
|
"tsup": "^7.2.0",
|
|
20
21
|
"typescript": "^5.3.2"
|
|
21
22
|
},
|
|
22
|
-
"dependencies": {
|
|
23
|
-
"@maxmind/geoip2-node": "^5.0.0"
|
|
24
|
-
}
|
|
23
|
+
"dependencies": {}
|
|
25
24
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { NextRequest } from "next/server";
|
|
2
2
|
import { WebServiceClient } from "@maxmind/geoip2-node";
|
|
3
|
-
import { ensureError } from "./utils";
|
|
3
|
+
import { ensureError, getBaseUrl } from "./utils";
|
|
4
4
|
|
|
5
5
|
type GeoLocation = {
|
|
6
6
|
countryCode: string;
|
|
@@ -12,8 +12,7 @@ export type AnalyticsEventBase = {
|
|
|
12
12
|
| "InterviewStarted"
|
|
13
13
|
| "ProtocolInstalled"
|
|
14
14
|
| "AppSetup"
|
|
15
|
-
| "Error"
|
|
16
|
-
| string;
|
|
15
|
+
| "Error";
|
|
17
16
|
};
|
|
18
17
|
|
|
19
18
|
export type AnalyticsEvent = AnalyticsEventBase & {
|
|
@@ -21,8 +20,7 @@ export type AnalyticsEvent = AnalyticsEventBase & {
|
|
|
21
20
|
| "InterviewCompleted"
|
|
22
21
|
| "InterviewStarted"
|
|
23
22
|
| "ProtocolInstalled"
|
|
24
|
-
| "AppSetup"
|
|
25
|
-
| string;
|
|
23
|
+
| "AppSetup";
|
|
26
24
|
metadata?: Record<string, unknown>;
|
|
27
25
|
};
|
|
28
26
|
|
|
@@ -48,83 +46,141 @@ export type DispatchableAnalyticsEvent = AnalyticsEventOrErrorWithTimestamp & {
|
|
|
48
46
|
};
|
|
49
47
|
|
|
50
48
|
type RouteHandlerConfiguration = {
|
|
51
|
-
maxMindAccountId: string;
|
|
52
|
-
maxMindLicenseKey: string;
|
|
53
49
|
platformUrl?: string;
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
installationId: string;
|
|
51
|
+
maxMindClient: WebServiceClient;
|
|
56
52
|
};
|
|
57
53
|
|
|
58
54
|
export const createRouteHandler = ({
|
|
59
|
-
maxMindAccountId,
|
|
60
|
-
maxMindLicenseKey,
|
|
61
55
|
platformUrl = "https://analytics.networkcanvas.com",
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
installationId,
|
|
57
|
+
maxMindClient,
|
|
64
58
|
}: RouteHandlerConfiguration) => {
|
|
65
59
|
return async (request: NextRequest) => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
60
|
+
try {
|
|
61
|
+
const event =
|
|
62
|
+
(await request.json()) as AnalyticsEventOrErrorWithTimestamp;
|
|
63
|
+
|
|
64
|
+
const ip = await fetch("https://api64.ipify.org").then((res) =>
|
|
65
|
+
res.text()
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const { country } = await maxMindClient.country(ip);
|
|
69
|
+
const countryCode = country?.isoCode ?? "Unknown";
|
|
70
|
+
|
|
71
|
+
const dispatchableEvent: DispatchableAnalyticsEvent = {
|
|
72
|
+
...event,
|
|
73
|
+
installationId,
|
|
74
|
+
geolocation: {
|
|
75
|
+
countryCode,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Forward to microservice
|
|
80
|
+
const response = await fetch(`${platformUrl}/api/event`, {
|
|
81
|
+
keepalive: true,
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify(dispatchableEvent),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
if (response.status === 404) {
|
|
91
|
+
console.error(
|
|
92
|
+
`Analytics platform not found. Please specify a valid platform URL.`
|
|
93
|
+
);
|
|
94
|
+
} else if (response.status === 500) {
|
|
95
|
+
console.error(
|
|
96
|
+
`Internal server error on analytics platform when forwarding event: ${response.statusText}.`
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
console.error(
|
|
100
|
+
`General error when forwarding event: ${response.statusText}`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return new Response(
|
|
105
|
+
JSON.stringify({ error: "Internal Server Error" }),
|
|
106
|
+
{
|
|
107
|
+
status: 500,
|
|
108
|
+
headers: {
|
|
109
|
+
"Content-Type": "application/json",
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
);
|
|
71
113
|
}
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
const installationId = await getInstallationId();
|
|
75
|
-
|
|
76
|
-
const event = (await request.json()) as AnalyticsEventOrErrorWithTimestamp;
|
|
77
|
-
|
|
78
|
-
const ip = await fetch("https://api64.ipify.org").then((res) => res.text());
|
|
79
|
-
|
|
80
|
-
const { country } = await maxMindClient.country(ip);
|
|
81
|
-
const countryCode = country?.isoCode ?? "Unknown";
|
|
82
114
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
console.info(`🚀 Analytics event forwarded successfully.`);
|
|
116
|
+
console.info(JSON.stringify(dispatchableEvent, null, 2));
|
|
117
|
+
|
|
118
|
+
return new Response(
|
|
119
|
+
JSON.stringify({ message: "Event forwarded successfully" }),
|
|
120
|
+
{
|
|
121
|
+
status: 200,
|
|
122
|
+
headers: {
|
|
123
|
+
"Content-Type": "application/json",
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
const error = ensureError(e);
|
|
129
|
+
console.error("Error in route handler:", error);
|
|
130
|
+
|
|
131
|
+
// Return an appropriate error response
|
|
132
|
+
return new Response(JSON.stringify({ error: "Internal Server Error" }), {
|
|
133
|
+
status: 500,
|
|
134
|
+
headers: {
|
|
135
|
+
"Content-Type": "application/json",
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
106
139
|
};
|
|
107
140
|
};
|
|
108
141
|
|
|
109
142
|
export const makeEventTracker =
|
|
110
|
-
(
|
|
111
|
-
(event: AnalyticsEventOrError) => {
|
|
143
|
+
(endpoint: string = "/api/analytics") =>
|
|
144
|
+
async (event: AnalyticsEventOrError) => {
|
|
145
|
+
const endpointWithHost = getBaseUrl() + endpoint;
|
|
146
|
+
|
|
112
147
|
const eventWithTimeStamp = {
|
|
113
148
|
...event,
|
|
114
149
|
timestamp: new Date(),
|
|
115
150
|
};
|
|
116
151
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
152
|
+
try {
|
|
153
|
+
const response = await fetch(endpointWithHost, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
keepalive: true,
|
|
156
|
+
body: JSON.stringify(eventWithTimeStamp),
|
|
157
|
+
headers: {
|
|
158
|
+
"Content-Type": "application/json",
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
if (response.status === 404) {
|
|
164
|
+
console.error(
|
|
165
|
+
`Analytics endpoint not found, did you forget to add the route?`
|
|
166
|
+
);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (response.status === 500) {
|
|
171
|
+
console.error(
|
|
172
|
+
`Internal server error when sending analytics event: ${response.statusText}. Check the route handler implementation.`
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.error(
|
|
178
|
+
`General error sending analytics event: ${response.statusText}`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {
|
|
125
182
|
const error = ensureError(e);
|
|
126
183
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
});
|
|
184
|
+
console.error("Internal error with analytics:", error.message);
|
|
185
|
+
}
|
|
130
186
|
};
|
package/src/utils.ts
CHANGED
|
@@ -17,3 +17,20 @@ export function ensureError(value: unknown): Error {
|
|
|
17
17
|
);
|
|
18
18
|
return error;
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
export function getBaseUrl() {
|
|
22
|
+
if (typeof window !== "undefined")
|
|
23
|
+
// browser should use relative path
|
|
24
|
+
return "";
|
|
25
|
+
|
|
26
|
+
if (process.env.VERCEL_URL)
|
|
27
|
+
// reference for vercel.com
|
|
28
|
+
return `https://${process.env.VERCEL_URL}`;
|
|
29
|
+
|
|
30
|
+
if (process.env.NEXT_PUBLIC_URL)
|
|
31
|
+
// Manually set deployment URL from env
|
|
32
|
+
return process.env.NEXT_PUBLIC_URL;
|
|
33
|
+
|
|
34
|
+
// assume localhost
|
|
35
|
+
return `http://127.0.0.1:3000`;
|
|
36
|
+
}
|