@heyramzi/marketing 3.0.0 → 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/README.md +7 -2
- package/package.json +1 -1
- package/src/analytics/server.test.ts +111 -228
- package/src/analytics/server.ts +58 -42
package/README.md
CHANGED
|
@@ -26,8 +26,13 @@ The analytics module provides wrappers for Umami tracking across different envir
|
|
|
26
26
|
```typescript
|
|
27
27
|
import { initServer, trackServer } from '@heyramzi/marketing/analytics/server';
|
|
28
28
|
|
|
29
|
-
initServer({
|
|
30
|
-
|
|
29
|
+
initServer({
|
|
30
|
+
hostUrl: 'https://umami.example.com',
|
|
31
|
+
websiteId: process.env.UMAMI_WEBSITE_ID,
|
|
32
|
+
hostname: 'example.com',
|
|
33
|
+
});
|
|
34
|
+
// Resolves true once Umami recorded it. On a Worker, await it or pass it to waitUntil.
|
|
35
|
+
await trackServer('signup', { plan: 'pro' }, { url: '/signup', id: user.id });
|
|
31
36
|
```
|
|
32
37
|
|
|
33
38
|
#### Script tag and browser tracking
|
package/package.json
CHANGED
|
@@ -1,282 +1,165 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vite-plus/test";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vite-plus/test";
|
|
2
2
|
|
|
3
|
-
// The server module holds module
|
|
4
|
-
// Each test resets them by calling vi.resetModules() + re-importing.
|
|
3
|
+
// The server module holds its config at module level, so each test re-imports it.
|
|
5
4
|
|
|
6
5
|
async function freshImport() {
|
|
7
6
|
vi.resetModules();
|
|
8
7
|
return import("#src/analytics/server.js");
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
vi.resetModules();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
// ---- initServer ----
|
|
16
|
-
|
|
17
|
-
describe("initServer", () => {
|
|
18
|
-
it("stores config so that subsequent trackServer calls succeed", async () => {
|
|
19
|
-
const mockTrack = vi.fn().mockResolvedValue(undefined);
|
|
20
|
-
|
|
21
|
-
vi.doMock("@umami/node", () => ({
|
|
22
|
-
Umami: class {
|
|
23
|
-
track = mockTrack;
|
|
24
|
-
},
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
const { initServer, trackServer } = await freshImport();
|
|
28
|
-
initServer({ hostUrl: "https://umami.example.com", websiteId: "abc-123" });
|
|
29
|
-
trackServer("test_event");
|
|
30
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
10
|
+
const fetchMock = vi.fn();
|
|
31
11
|
|
|
32
|
-
|
|
33
|
-
});
|
|
12
|
+
function answer(body: string, status = 200) {
|
|
13
|
+
fetchMock.mockResolvedValue(new Response(body, { status }));
|
|
14
|
+
}
|
|
34
15
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
16
|
+
function sent() {
|
|
17
|
+
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
18
|
+
return {
|
|
19
|
+
url,
|
|
20
|
+
headers: init.headers as Record<string, string>,
|
|
21
|
+
body: JSON.parse(init.body as string) as { type: string; payload: Record<string, unknown> },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
38
24
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
fetchMock.mockReset();
|
|
27
|
+
answer('{"cache":"x"}');
|
|
28
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
29
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
30
|
+
});
|
|
42
31
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
47
|
-
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.unstubAllGlobals();
|
|
34
|
+
vi.restoreAllMocks();
|
|
48
35
|
});
|
|
49
36
|
|
|
50
|
-
|
|
37
|
+
const config = {
|
|
38
|
+
hostUrl: "https://umami.example.com",
|
|
39
|
+
websiteId: "abc-123",
|
|
40
|
+
hostname: "example.com",
|
|
41
|
+
};
|
|
51
42
|
|
|
52
43
|
describe("trackServer", () => {
|
|
53
|
-
it("
|
|
54
|
-
const mockTrack = vi.fn().mockResolvedValue(undefined);
|
|
55
|
-
vi.doMock("@umami/node", () => ({
|
|
56
|
-
Umami: class {
|
|
57
|
-
track = mockTrack;
|
|
58
|
-
},
|
|
59
|
-
}));
|
|
60
|
-
|
|
44
|
+
it("awaits the POST to /api/send and resolves true", async () => {
|
|
61
45
|
const { initServer, trackServer } = await freshImport();
|
|
62
|
-
initServer(
|
|
63
|
-
trackServer("signup");
|
|
64
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
65
|
-
|
|
66
|
-
expect(mockTrack).toHaveBeenCalledWith({ name: "signup" });
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("includes data in the payload when provided", async () => {
|
|
70
|
-
const mockTrack = vi.fn().mockResolvedValue(undefined);
|
|
71
|
-
vi.doMock("@umami/node", () => ({
|
|
72
|
-
Umami: class {
|
|
73
|
-
track = mockTrack;
|
|
74
|
-
},
|
|
75
|
-
}));
|
|
46
|
+
initServer(config);
|
|
76
47
|
|
|
77
|
-
|
|
78
|
-
initServer({ hostUrl: "https://umami.example.com", websiteId: "abc-123" });
|
|
79
|
-
trackServer("feature_used", { feature: "export", count: 5 });
|
|
80
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
48
|
+
await expect(trackServer("signup")).resolves.toBe(true);
|
|
81
49
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
50
|
+
const { url, headers, body } = sent();
|
|
51
|
+
expect(url).toBe("https://umami.example.com/api/send");
|
|
52
|
+
expect(headers["User-Agent"]).toMatch(/^Mozilla\/5\.0/);
|
|
53
|
+
expect(body).toEqual({
|
|
54
|
+
type: "event",
|
|
55
|
+
payload: { website: "abc-123", hostname: "example.com", url: "/", name: "signup" },
|
|
85
56
|
});
|
|
86
57
|
});
|
|
87
58
|
|
|
88
|
-
it("
|
|
89
|
-
const mockTrack = vi.fn().mockResolvedValue(undefined);
|
|
90
|
-
vi.doMock("@umami/node", () => ({
|
|
91
|
-
Umami: class {
|
|
92
|
-
track = mockTrack;
|
|
93
|
-
},
|
|
94
|
-
}));
|
|
95
|
-
|
|
59
|
+
it("sends data, url and id", async () => {
|
|
96
60
|
const { initServer, trackServer } = await freshImport();
|
|
97
|
-
initServer(
|
|
98
|
-
trackServer("page_view", undefined, "/pricing");
|
|
99
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
100
|
-
|
|
101
|
-
expect(mockTrack).toHaveBeenCalledWith({ name: "page_view", url: "/pricing" });
|
|
102
|
-
});
|
|
61
|
+
initServer(config);
|
|
103
62
|
|
|
104
|
-
|
|
105
|
-
const mockTrack = vi.fn().mockResolvedValue(undefined);
|
|
106
|
-
vi.doMock("@umami/node", () => ({
|
|
107
|
-
Umami: class {
|
|
108
|
-
track = mockTrack;
|
|
109
|
-
},
|
|
110
|
-
}));
|
|
63
|
+
await trackServer("click", { label: "CTA" }, { url: "/home", id: "cs_123" });
|
|
111
64
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
expect(mockTrack).toHaveBeenCalledWith({
|
|
65
|
+
expect(sent().body.payload).toEqual({
|
|
66
|
+
website: "abc-123",
|
|
67
|
+
hostname: "example.com",
|
|
68
|
+
url: "/home",
|
|
118
69
|
name: "click",
|
|
119
70
|
data: { label: "CTA" },
|
|
120
|
-
|
|
71
|
+
id: "cs_123",
|
|
121
72
|
});
|
|
122
73
|
});
|
|
123
74
|
|
|
124
|
-
it("
|
|
125
|
-
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
126
|
-
const mockTrack = vi.fn().mockRejectedValue(new Error("network failure"));
|
|
127
|
-
vi.doMock("@umami/node", () => ({
|
|
128
|
-
Umami: class {
|
|
129
|
-
track = mockTrack;
|
|
130
|
-
},
|
|
131
|
-
}));
|
|
132
|
-
|
|
75
|
+
it("still takes the url as a plain string", async () => {
|
|
133
76
|
const { initServer, trackServer } = await freshImport();
|
|
134
|
-
initServer(
|
|
135
|
-
trackServer("risky_event");
|
|
136
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
77
|
+
initServer(config);
|
|
137
78
|
|
|
138
|
-
|
|
139
|
-
});
|
|
79
|
+
await trackServer("page_view", undefined, "/pricing");
|
|
140
80
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
vi.doMock("@umami/node", () => ({
|
|
145
|
-
Umami: class {
|
|
146
|
-
constructor() {
|
|
147
|
-
constructorCallCount++;
|
|
148
|
-
}
|
|
149
|
-
track = mockTrack;
|
|
150
|
-
},
|
|
151
|
-
}));
|
|
81
|
+
expect(sent().body.payload.url).toBe("/pricing");
|
|
82
|
+
expect(sent().body.payload).not.toHaveProperty("id");
|
|
83
|
+
});
|
|
152
84
|
|
|
85
|
+
it("strips a trailing slash from hostUrl and forwards a custom userAgent", async () => {
|
|
153
86
|
const { initServer, trackServer } = await freshImport();
|
|
154
|
-
initServer({
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
160
|
-
|
|
161
|
-
expect(constructorCallCount).toBe(1);
|
|
162
|
-
expect(mockTrack).toHaveBeenCalledTimes(2);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
87
|
+
initServer({
|
|
88
|
+
...config,
|
|
89
|
+
hostUrl: "https://umami.example.com/",
|
|
90
|
+
userAgent: "Mozilla/5.0 Seam/1.0",
|
|
91
|
+
});
|
|
165
92
|
|
|
166
|
-
|
|
93
|
+
await trackServer("ua_test");
|
|
167
94
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
vi.doMock("@umami/node", () => ({
|
|
172
|
-
Umami: class {
|
|
173
|
-
track = mockTrack;
|
|
174
|
-
},
|
|
175
|
-
}));
|
|
95
|
+
expect(sent().url).toBe("https://umami.example.com/api/send");
|
|
96
|
+
expect(sent().headers["User-Agent"]).toBe("Mozilla/5.0 Seam/1.0");
|
|
97
|
+
});
|
|
176
98
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
99
|
+
// Umami rejects an event with HTTP 200 and the body `beep`.
|
|
100
|
+
it("resolves false when Umami answers beep", async () => {
|
|
101
|
+
answer("beep");
|
|
102
|
+
const { initServer, trackServer } = await freshImport();
|
|
103
|
+
initServer(config);
|
|
181
104
|
|
|
182
|
-
expect(
|
|
183
|
-
|
|
184
|
-
data: { revenue: 49.99, currency: "USD" },
|
|
185
|
-
});
|
|
105
|
+
await expect(trackServer("dropped")).resolves.toBe(false);
|
|
106
|
+
expect(console.error).toHaveBeenCalled();
|
|
186
107
|
});
|
|
187
108
|
|
|
188
|
-
it("
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
track = mockTrack;
|
|
193
|
-
},
|
|
194
|
-
}));
|
|
109
|
+
it("resolves false on an HTTP error", async () => {
|
|
110
|
+
answer("nope", 500);
|
|
111
|
+
const { initServer, trackServer } = await freshImport();
|
|
112
|
+
initServer(config);
|
|
195
113
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
trackServerRevenue("subscription", 9.99, "EUR", { plan: "pro" });
|
|
199
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
114
|
+
await expect(trackServer("server_error")).resolves.toBe(false);
|
|
115
|
+
});
|
|
200
116
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
117
|
+
it("resolves false and never throws when fetch rejects", async () => {
|
|
118
|
+
fetchMock.mockRejectedValue(new Error("network failure"));
|
|
119
|
+
const { initServer, trackServer } = await freshImport();
|
|
120
|
+
initServer(config);
|
|
121
|
+
|
|
122
|
+
await expect(trackServer("risky_event")).resolves.toBe(false);
|
|
205
123
|
});
|
|
206
124
|
|
|
207
|
-
it("
|
|
208
|
-
const
|
|
209
|
-
vi.doMock("@umami/node", () => ({
|
|
210
|
-
Umami: class {
|
|
211
|
-
track = mockTrack;
|
|
212
|
-
},
|
|
213
|
-
}));
|
|
125
|
+
it("resolves false without initServer and sends nothing", async () => {
|
|
126
|
+
const { trackServer } = await freshImport();
|
|
214
127
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
trackServerRevenue("checkout", 19.99, "USD", undefined, "/checkout");
|
|
218
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
219
|
-
|
|
220
|
-
expect(mockTrack).toHaveBeenCalledWith({
|
|
221
|
-
name: "checkout",
|
|
222
|
-
data: { revenue: 19.99, currency: "USD" },
|
|
223
|
-
url: "/checkout",
|
|
224
|
-
});
|
|
128
|
+
await expect(trackServer("orphan_event")).resolves.toBe(false);
|
|
129
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
225
130
|
});
|
|
226
131
|
});
|
|
227
132
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
expect(capturedConfig).toEqual({
|
|
249
|
-
hostUrl: "https://analytics.myapp.io",
|
|
250
|
-
websiteId: "xyz-789",
|
|
251
|
-
userAgent: "Server",
|
|
133
|
+
describe("trackServerRevenue", () => {
|
|
134
|
+
it("merges revenue and currency into data and forwards url and id", async () => {
|
|
135
|
+
const { initServer, trackServerRevenue } = await freshImport();
|
|
136
|
+
initServer(config);
|
|
137
|
+
|
|
138
|
+
await expect(
|
|
139
|
+
trackServerRevenue(
|
|
140
|
+
"Purchase Complete",
|
|
141
|
+
9.99,
|
|
142
|
+
"EUR",
|
|
143
|
+
{ plan: "pro" },
|
|
144
|
+
{ url: "/buy", id: "cs_1" },
|
|
145
|
+
),
|
|
146
|
+
).resolves.toBe(true);
|
|
147
|
+
|
|
148
|
+
expect(sent().body.payload).toMatchObject({
|
|
149
|
+
name: "Purchase Complete",
|
|
150
|
+
url: "/buy",
|
|
151
|
+
id: "cs_1",
|
|
152
|
+
data: { plan: "pro", revenue: 9.99, currency: "EUR" },
|
|
252
153
|
});
|
|
253
154
|
});
|
|
254
155
|
|
|
255
|
-
it("
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
Umami: class {
|
|
260
|
-
constructor(cfg: unknown) {
|
|
261
|
-
capturedConfig = cfg;
|
|
262
|
-
}
|
|
263
|
-
track = mockTrack;
|
|
264
|
-
},
|
|
265
|
-
}));
|
|
266
|
-
|
|
267
|
-
const { initServer, trackServer } = await freshImport();
|
|
268
|
-
initServer({
|
|
269
|
-
hostUrl: "https://umami.example.com",
|
|
270
|
-
websiteId: "abc-123",
|
|
271
|
-
userAgent: "MyBot/1.0",
|
|
272
|
-
});
|
|
273
|
-
trackServer("ua_test");
|
|
274
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
156
|
+
it("resolves false when the send fails", async () => {
|
|
157
|
+
answer("beep");
|
|
158
|
+
const { initServer, trackServerRevenue } = await freshImport();
|
|
159
|
+
initServer(config);
|
|
275
160
|
|
|
276
|
-
expect(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
userAgent: "MyBot/1.0",
|
|
280
|
-
});
|
|
161
|
+
await expect(
|
|
162
|
+
trackServerRevenue("checkout", 19.99, "USD", undefined, "/checkout"),
|
|
163
|
+
).resolves.toBe(false);
|
|
281
164
|
});
|
|
282
165
|
});
|
package/src/analytics/server.ts
CHANGED
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Server-side Umami tracking
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Requires `@umami/node` peer dependency.
|
|
2
|
+
* Server-side Umami tracking. A Worker cancels a fetch still in flight once the response
|
|
3
|
+
* returns, so await each tracker or hand it to `waitUntil`.
|
|
6
4
|
*/
|
|
7
5
|
|
|
8
6
|
import type { EventData } from "#src/analytics/types.js";
|
|
9
|
-
|
|
10
|
-
type UmamiNodeClient = {
|
|
11
|
-
track: (payload: Record<string, unknown>) => Promise<unknown>;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
type UmamiNodeModule = {
|
|
15
|
-
Umami: new (config: {
|
|
16
|
-
hostUrl: string;
|
|
17
|
-
websiteId: string;
|
|
18
|
-
userAgent?: string;
|
|
19
|
-
}) => UmamiNodeClient;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
let client: UmamiNodeClient | null = null;
|
|
7
|
+
import { buildSendBody, UMAMI_ENDPOINTS } from "#src/analytics/umami/protocol.js";
|
|
23
8
|
|
|
24
9
|
type ServerConfig = {
|
|
25
10
|
hostUrl: string;
|
|
26
11
|
websiteId: string;
|
|
12
|
+
/** Site hostname, such as `getseam.app`. Without it Umami logs the event under `localhost`. */
|
|
13
|
+
hostname?: string;
|
|
14
|
+
/** Umami answers a non-browser user agent with `beep` and drops the event. */
|
|
27
15
|
userAgent?: string;
|
|
28
16
|
};
|
|
29
17
|
|
|
18
|
+
/** Per-event options. A string is the url, as in 3.0. */
|
|
19
|
+
export type ServerEventOptions = {
|
|
20
|
+
url?: string;
|
|
21
|
+
/** Umami keys the session on it. Without one, every event shares the Worker's session. */
|
|
22
|
+
id?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Umami's bot filter answers `beep` to any agent containing "Server" (tested 19 Oct 2026).
|
|
26
|
+
const DEFAULT_USER_AGENT = "Mozilla/5.0 Marketing/1.0";
|
|
27
|
+
const TIMEOUT_MS = 3000;
|
|
28
|
+
|
|
30
29
|
let _config: ServerConfig | null = null;
|
|
31
30
|
|
|
32
31
|
/** Initialize the server-side Umami client. Call once at startup. */
|
|
@@ -34,30 +33,47 @@ export function initServer(config: ServerConfig): void {
|
|
|
34
33
|
_config = config;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Sends one event and checks Umami recorded it. Never throws.
|
|
38
|
+
* Resolves false on a timeout, an HTTP error or a `beep` rejection.
|
|
39
|
+
*/
|
|
40
|
+
export async function trackServer(
|
|
41
|
+
name: string,
|
|
42
|
+
data?: EventData,
|
|
43
|
+
options?: string | ServerEventOptions,
|
|
44
|
+
): Promise<boolean> {
|
|
45
|
+
const { url, id } =
|
|
46
|
+
typeof options === "string" ? { url: options, id: undefined } : (options ?? {});
|
|
47
|
+
try {
|
|
48
|
+
if (!_config) throw new Error("Call initServer() before tracking events");
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
const body = buildSendBody({
|
|
51
|
+
websiteId: _config.websiteId,
|
|
52
|
+
hostname: _config.hostname,
|
|
53
|
+
name,
|
|
54
|
+
url,
|
|
55
|
+
data,
|
|
56
|
+
});
|
|
57
|
+
const payload = id ? { ...body.payload, id } : body.payload;
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
const response = await fetch(`${_config.hostUrl.replace(/\/$/, "")}${UMAMI_ENDPOINTS.send}`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
"User-Agent": _config.userAgent ?? DEFAULT_USER_AGENT,
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({ ...body, payload }),
|
|
66
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
60
67
|
});
|
|
68
|
+
const text = await response.text();
|
|
69
|
+
if (!response.ok || text.includes("beep")) {
|
|
70
|
+
throw new Error(`HTTP ${response.status}: ${text.slice(0, 120)}`);
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
} catch (err: unknown) {
|
|
74
|
+
console.error(`[analytics] Server track failed for ${name}:`, err);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
/** Track a server-side revenue event. Revenue must be in dollars (not cents). */
|
|
@@ -66,7 +82,7 @@ export function trackServerRevenue(
|
|
|
66
82
|
revenue: number,
|
|
67
83
|
currency: string,
|
|
68
84
|
data?: EventData,
|
|
69
|
-
|
|
70
|
-
):
|
|
71
|
-
trackServer(name, { ...data, revenue, currency },
|
|
85
|
+
options?: string | ServerEventOptions,
|
|
86
|
+
): Promise<boolean> {
|
|
87
|
+
return trackServer(name, { ...data, revenue, currency }, options);
|
|
72
88
|
}
|