midline-agent 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ All notable changes to `midline-agent`, compiled from git history. Dates reflect commit dates, newest first.
4
+
5
+ ## 2026-09-17
6
+ - feat: `midline-agent/browser` captures pageviews — on load and on every SPA route change — as the
7
+ basis for visitor counting and funnel views in the Midline dashboard (0.5.0)
8
+ - feat: every browser event carries a top-level `sessionId`, stable for the tab's lifetime
9
+
10
+ ## 2026-09-14
11
+ - chore: bump version to 0.4.1
12
+ - chore: add socket.io dependencies and update configuration
13
+
14
+ ## 2026-09-13
15
+ - feat: midline-agent/browser, a browser SDK in the same package (0.4.0)
16
+ - docs: console lines show on the Terminal page, and 5xx responses group into Issues
17
+ - docs: add README for midline-agent SDK
18
+ - feat!: midline-agent 0.2.0 — verified TLS only, proxy mode, safe delivery (breaking)
19
+ - feat: add midline-agent package with core types, configuration, and agent implementation
20
+
21
+ ## 2026-09-12
22
+ - first commit
23
+ - feat: add midline-agent package for event monitoring and reporting
24
+ - Update agent API endpoint to api.usemidline.com
25
+ - docs: expand README with detailed NestJS, Node.js, and Express.js setup guides
package/README.md CHANGED
@@ -378,9 +378,10 @@ app.get("/x", (req, res) => { logger.info({ requestId: getRequestContext(req)?.r
378
378
  ## Browser apps
379
379
 
380
380
  `midline-agent/browser` monitors a web app from the page: uncaught errors and unhandled rejections,
381
- failed `fetch` and `XMLHttpRequest` calls, Web Vitals (LCP, INP, CLS, FCP, TTFB) and, if you ask,
382
- console output. It has no dependencies, no Node code, and works under React, Vue, Angular, Svelte,
383
- Next.js or no framework at all, because it instruments the page rather than a framework.
381
+ failed `fetch` and `XMLHttpRequest` calls, Web Vitals (LCP, INP, CLS, FCP, TTFB), pageviews for
382
+ visitor counting and funnel views, and, if you ask, console output. It has no dependencies, no Node
383
+ code, and works under React, Vue, Angular, Svelte, Next.js or no framework at all, because it
384
+ instruments the page rather than a framework.
384
385
 
385
386
  ```ts
386
387
  import * as Midline from "midline-agent/browser";
@@ -424,6 +425,7 @@ the browser will block the call's preflight.
424
425
  | `captureErrors` | `true` | Uncaught errors and unhandled rejections. |
425
426
  | `captureRequests` | `"failed"` | `"failed"` (4xx, 5xx, network errors), `"all"`, or `false`. Every call is a breadcrumb either way. |
426
427
  | `captureWebVitals` | `true` | Reported once, when the page is first hidden. |
428
+ | `capturePageviews` | `true` | A `pageview` event on load and on every SPA route change (`pushState`/`replaceState`/`popstate`), tagged with a per-tab `sessionId` — what the dashboard's visitor and funnel views are built from. |
427
429
  | `captureConsole` | `false` | `true` for `error` and `warn`, or a list of levels. Lines appear on the Terminal page. Opt-in because wrapped console calls show the SDK as their source in devtools. |
428
430
  | `tracePropagationTargets` | same origin | Strings match as URL prefixes (or path prefixes starting with `/`); RegExps match the full URL. |
429
431
  | `ignoreErrors`, `ignoreUrls` | `[]` | Strings match as substrings. |
@@ -446,7 +448,7 @@ No bundler? Load the ES module build directly:
446
448
 
447
449
  ```html
448
450
  <script type="module">
449
- import * as Midline from "https://cdn.jsdelivr.net/npm/midline-agent@0.4.0/dist/esm/browser/index.js";
451
+ import * as Midline from "https://cdn.jsdelivr.net/npm/midline-agent@0.5.0/dist/esm/browser/index.js";
450
452
  Midline.init({ apiKey: "pk_…", service: "web" });
451
453
  </script>
452
454
  ```
@@ -476,8 +478,8 @@ Authenticate with the `X-API-Key` header. An `apiKey` field in the body is still
476
478
 
477
479
  | Field | Required | Notes |
478
480
  | --- | --- | --- |
479
- | `eventType` | yes | `request` · `error` · `security` · `performance` · `custom` |
480
- | `route` | yes | Path only, e.g. `/v1/charges` |
481
+ | `eventType` | yes | `request` · `error` · `security` · `performance` · `pageview` · `custom` |
482
+ | `route` | yes | Path only, e.g. `/v1/charges`. For `pageview`, the page path. |
481
483
  | `method` | no | HTTP method |
482
484
  | `statusCode` | no | 100–599 |
483
485
  | `responseTime` | no | Milliseconds |
@@ -486,6 +488,7 @@ Authenticate with the `X-API-Key` header. An `apiKey` field in the body is still
486
488
  | `service` / `environment` / `release` | no | Where it came from |
487
489
  | `timestamp` | no | ISO 8601; clamped to server time if implausibly far off |
488
490
  | `requestId` / `correlationId` / `traceId` | no | For joining events across services |
491
+ | `sessionId` | no | A stable id for one visitor's session (e.g. a browser tab). Send the same value on every event from that session and Midline can build visitor counts and funnels from `pageview` events. Keep it opaque — never an email or a real user id. |
489
492
  | `payload` | no | Free-form context — error message, stack, captured request/response |
490
493
  | `metadata` | no | Free-form, e.g. SDK name and version |
491
494
 
@@ -41,6 +41,7 @@ export declare class BrowserClient {
41
41
  private handleRequest;
42
42
  private handleConsole;
43
43
  private handleNavigation;
44
+ private handlePageview;
44
45
  private handleVitals;
45
46
  private propagation;
46
47
  private base;
@@ -26,7 +26,7 @@ const CONSOLE_SEVERITY = {
26
26
  /** Top-level fields the ingest API accepts from this SDK; it rejects the whole batch on anything else. */
27
27
  const WIRE_FIELDS = new Set([
28
28
  "eventType", "route", "method", "statusCode", "responseTime", "severity", "category", "timestamp",
29
- "service", "environment", "release", "userAgent", "traceId", "spanId", "payload", "metadata",
29
+ "service", "environment", "release", "userAgent", "traceId", "spanId", "sessionId", "payload", "metadata",
30
30
  ]);
31
31
  class BrowserConfigError extends Error {
32
32
  }
@@ -81,6 +81,7 @@ function resolve(config) {
81
81
  captureRequests: config.captureRequests === undefined ? "failed" : config.captureRequests,
82
82
  consoleLevels: levels.filter((level) => level in CONSOLE_SEVERITY),
83
83
  captureWebVitals: config.captureWebVitals !== false,
84
+ capturePageviews: config.capturePageviews !== false,
84
85
  tracePropagationTargets: config.tracePropagationTargets,
85
86
  ignoreErrors: config.ignoreErrors ?? [],
86
87
  ignoreUrls: config.ignoreUrls ?? [],
@@ -169,6 +170,9 @@ class BrowserClient {
169
170
  if (this.config.captureWebVitals) {
170
171
  this.teardowns.push((0, vitals_js_1.observeVitals)(this.win, (report) => this.handleVitals(report)));
171
172
  }
173
+ if (this.config.capturePageviews) {
174
+ this.handlePageview(this.currentRoute(), this.win.document?.referrer || undefined);
175
+ }
172
176
  // Registered after the vitals listener so its event is queued before this sends.
173
177
  const onHide = () => {
174
178
  if (this.win.document?.visibilityState === "hidden")
@@ -327,6 +331,16 @@ class BrowserClient {
327
331
  // A new view is a new trace, so its API calls don't blur into the last one's.
328
332
  this.traceId = randomHex(16);
329
333
  this.breadcrumb("navigation", `${pathOf(from)} -> ${pathOf(to)}`);
334
+ // history.pushState/replaceState have already run by the time this fires, so
335
+ // win.location — and therefore currentRoute() — reflects the new view.
336
+ if (this.config.capturePageviews)
337
+ this.handlePageview(this.currentRoute());
338
+ }
339
+ handlePageview(route, referrer) {
340
+ const event = this.base("pageview", route, "low", "application");
341
+ if (referrer)
342
+ event.payload = { referrer: this.redactor.string(referrer, 2048) };
343
+ this.emit(event);
330
344
  }
331
345
  handleVitals(report) {
332
346
  const poor = Object.values(report.vitals).some((vital) => vital?.rating === "poor");
@@ -356,7 +370,6 @@ class BrowserClient {
356
370
  sdk: "midline-agent/browser",
357
371
  version: version_js_1.BROWSER_SDK_VERSION,
358
372
  runtime: "browser",
359
- sessionId: this.sessionId,
360
373
  page: { url: this.pageUrl(), path: this.currentRoute() },
361
374
  };
362
375
  if (this.user && (this.user.id || this.user.username))
@@ -375,6 +388,7 @@ class BrowserClient {
375
388
  release: this.config.release,
376
389
  userAgent: clamp(nav?.userAgent, 512),
377
390
  traceId: this.traceId,
391
+ sessionId: this.sessionId,
378
392
  metadata,
379
393
  };
380
394
  }
@@ -36,6 +36,12 @@ export interface MidlineBrowserConfig {
36
36
  captureConsole?: boolean | ConsoleLevel[];
37
37
  /** LCP, INP, CLS, FCP and TTFB, reported once when the page is first hidden. Default true. */
38
38
  captureWebVitals?: boolean;
39
+ /**
40
+ * A `pageview` event on load and on every SPA route change (history push/replace/popstate),
41
+ * carrying the same per-tab `sessionId` as every other event. This is what a visitor-count or
42
+ * funnel view in the Midline dashboard is built from. Default true.
43
+ */
44
+ capturePageviews?: boolean;
39
45
  /**
40
46
  * Requests that get a W3C `traceparent` header, so the backend's midline-agent
41
47
  * links its request to this page. Strings match as URL prefixes (or path prefixes
@@ -71,7 +77,7 @@ export interface MidlineBrowserConfig {
71
77
  }
72
78
  /** An event as it is sent to the ingest API. */
73
79
  export interface BrowserEvent {
74
- eventType: "error" | "request" | "console" | "performance" | "custom";
80
+ eventType: "error" | "request" | "console" | "performance" | "pageview" | "custom";
75
81
  route: string;
76
82
  method?: string;
77
83
  statusCode?: number;
@@ -85,6 +91,8 @@ export interface BrowserEvent {
85
91
  userAgent?: string;
86
92
  traceId?: string;
87
93
  spanId?: string;
94
+ /** Stable for the tab's lifetime (sessionStorage-backed). Same value on every event this SDK sends. */
95
+ sessionId?: string;
88
96
  payload?: Record<string, unknown>;
89
97
  metadata: Record<string, unknown>;
90
98
  }
@@ -1,2 +1,2 @@
1
1
  /** Kept in step with package.json by test/browser.test.js; a browser bundle can't read package.json. */
2
- export declare const BROWSER_SDK_VERSION = "0.4.0";
2
+ export declare const BROWSER_SDK_VERSION = "0.5.0";
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BROWSER_SDK_VERSION = void 0;
4
4
  /** Kept in step with package.json by test/browser.test.js; a browser bundle can't read package.json. */
5
- exports.BROWSER_SDK_VERSION = "0.4.0";
5
+ exports.BROWSER_SDK_VERSION = "0.5.0";
@@ -21,7 +21,7 @@ const CONSOLE_SEVERITY = {
21
21
  /** Top-level fields the ingest API accepts from this SDK; it rejects the whole batch on anything else. */
22
22
  const WIRE_FIELDS = new Set([
23
23
  "eventType", "route", "method", "statusCode", "responseTime", "severity", "category", "timestamp",
24
- "service", "environment", "release", "userAgent", "traceId", "spanId", "payload", "metadata",
24
+ "service", "environment", "release", "userAgent", "traceId", "spanId", "sessionId", "payload", "metadata",
25
25
  ]);
26
26
  export class BrowserConfigError extends Error {
27
27
  }
@@ -75,6 +75,7 @@ function resolve(config) {
75
75
  captureRequests: config.captureRequests === undefined ? "failed" : config.captureRequests,
76
76
  consoleLevels: levels.filter((level) => level in CONSOLE_SEVERITY),
77
77
  captureWebVitals: config.captureWebVitals !== false,
78
+ capturePageviews: config.capturePageviews !== false,
78
79
  tracePropagationTargets: config.tracePropagationTargets,
79
80
  ignoreErrors: config.ignoreErrors ?? [],
80
81
  ignoreUrls: config.ignoreUrls ?? [],
@@ -163,6 +164,9 @@ export class BrowserClient {
163
164
  if (this.config.captureWebVitals) {
164
165
  this.teardowns.push(observeVitals(this.win, (report) => this.handleVitals(report)));
165
166
  }
167
+ if (this.config.capturePageviews) {
168
+ this.handlePageview(this.currentRoute(), this.win.document?.referrer || undefined);
169
+ }
166
170
  // Registered after the vitals listener so its event is queued before this sends.
167
171
  const onHide = () => {
168
172
  if (this.win.document?.visibilityState === "hidden")
@@ -321,6 +325,16 @@ export class BrowserClient {
321
325
  // A new view is a new trace, so its API calls don't blur into the last one's.
322
326
  this.traceId = randomHex(16);
323
327
  this.breadcrumb("navigation", `${pathOf(from)} -> ${pathOf(to)}`);
328
+ // history.pushState/replaceState have already run by the time this fires, so
329
+ // win.location — and therefore currentRoute() — reflects the new view.
330
+ if (this.config.capturePageviews)
331
+ this.handlePageview(this.currentRoute());
332
+ }
333
+ handlePageview(route, referrer) {
334
+ const event = this.base("pageview", route, "low", "application");
335
+ if (referrer)
336
+ event.payload = { referrer: this.redactor.string(referrer, 2048) };
337
+ this.emit(event);
324
338
  }
325
339
  handleVitals(report) {
326
340
  const poor = Object.values(report.vitals).some((vital) => vital?.rating === "poor");
@@ -350,7 +364,6 @@ export class BrowserClient {
350
364
  sdk: "midline-agent/browser",
351
365
  version: BROWSER_SDK_VERSION,
352
366
  runtime: "browser",
353
- sessionId: this.sessionId,
354
367
  page: { url: this.pageUrl(), path: this.currentRoute() },
355
368
  };
356
369
  if (this.user && (this.user.id || this.user.username))
@@ -369,6 +382,7 @@ export class BrowserClient {
369
382
  release: this.config.release,
370
383
  userAgent: clamp(nav?.userAgent, 512),
371
384
  traceId: this.traceId,
385
+ sessionId: this.sessionId,
372
386
  metadata,
373
387
  };
374
388
  }
@@ -1,2 +1,2 @@
1
1
  /** Kept in step with package.json by test/browser.test.js; a browser bundle can't read package.json. */
2
- export const BROWSER_SDK_VERSION = "0.4.0";
2
+ export const BROWSER_SDK_VERSION = "0.5.0";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "midline-agent",
3
- "version": "0.4.1",
4
- "description": "Midline — request, error and security monitoring for Node, and error, network and Web Vitals monitoring for browsers",
3
+ "version": "0.5.0",
4
+ "description": "Midline — request, error and security monitoring for Node, and error, network, Web Vitals and pageview/visitor monitoring for browsers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
@@ -44,7 +44,7 @@ const CONSOLE_SEVERITY: Record<ConsoleLevel, EventSeverity> = {
44
44
  /** Top-level fields the ingest API accepts from this SDK; it rejects the whole batch on anything else. */
45
45
  const WIRE_FIELDS = new Set<string>([
46
46
  "eventType", "route", "method", "statusCode", "responseTime", "severity", "category", "timestamp",
47
- "service", "environment", "release", "userAgent", "traceId", "spanId", "payload", "metadata",
47
+ "service", "environment", "release", "userAgent", "traceId", "spanId", "sessionId", "payload", "metadata",
48
48
  ]);
49
49
 
50
50
  interface Resolved {
@@ -58,6 +58,7 @@ interface Resolved {
58
58
  captureRequests: RequestCapture;
59
59
  consoleLevels: ConsoleLevel[];
60
60
  captureWebVitals: boolean;
61
+ capturePageviews: boolean;
61
62
  tracePropagationTargets?: Array<string | RegExp>;
62
63
  ignoreErrors: Array<string | RegExp>;
63
64
  ignoreUrls: Array<string | RegExp>;
@@ -124,6 +125,7 @@ function resolve(config: MidlineBrowserConfig): Resolved {
124
125
  captureRequests: config.captureRequests === undefined ? "failed" : config.captureRequests,
125
126
  consoleLevels: levels.filter((level) => level in CONSOLE_SEVERITY),
126
127
  captureWebVitals: config.captureWebVitals !== false,
128
+ capturePageviews: config.capturePageviews !== false,
127
129
  tracePropagationTargets: config.tracePropagationTargets,
128
130
  ignoreErrors: config.ignoreErrors ?? [],
129
131
  ignoreUrls: config.ignoreUrls ?? [],
@@ -222,6 +224,9 @@ export class BrowserClient {
222
224
  if (this.config.captureWebVitals) {
223
225
  this.teardowns.push(observeVitals(this.win, (report) => this.handleVitals(report)));
224
226
  }
227
+ if (this.config.capturePageviews) {
228
+ this.handlePageview(this.currentRoute(), this.win.document?.referrer || undefined);
229
+ }
225
230
 
226
231
  // Registered after the vitals listener so its event is queued before this sends.
227
232
  const onHide = () => {
@@ -399,6 +404,15 @@ export class BrowserClient {
399
404
  // A new view is a new trace, so its API calls don't blur into the last one's.
400
405
  this.traceId = randomHex(16);
401
406
  this.breadcrumb("navigation", `${pathOf(from)} -> ${pathOf(to)}`);
407
+ // history.pushState/replaceState have already run by the time this fires, so
408
+ // win.location — and therefore currentRoute() — reflects the new view.
409
+ if (this.config.capturePageviews) this.handlePageview(this.currentRoute());
410
+ }
411
+
412
+ private handlePageview(route: string, referrer?: string): void {
413
+ const event = this.base("pageview", route, "low", "application");
414
+ if (referrer) event.payload = { referrer: this.redactor.string(referrer, 2048) };
415
+ this.emit(event);
402
416
  }
403
417
 
404
418
  private handleVitals(report: VitalsReport): void {
@@ -438,7 +452,6 @@ export class BrowserClient {
438
452
  sdk: "midline-agent/browser",
439
453
  version: BROWSER_SDK_VERSION,
440
454
  runtime: "browser",
441
- sessionId: this.sessionId,
442
455
  page: { url: this.pageUrl(), path: this.currentRoute() },
443
456
  };
444
457
  if (this.user && (this.user.id || this.user.username)) metadata.user = { ...this.user };
@@ -456,6 +469,7 @@ export class BrowserClient {
456
469
  release: this.config.release,
457
470
  userAgent: clamp(nav?.userAgent, 512),
458
471
  traceId: this.traceId,
472
+ sessionId: this.sessionId,
459
473
  metadata,
460
474
  };
461
475
  }
@@ -42,6 +42,12 @@ export interface MidlineBrowserConfig {
42
42
  captureConsole?: boolean | ConsoleLevel[];
43
43
  /** LCP, INP, CLS, FCP and TTFB, reported once when the page is first hidden. Default true. */
44
44
  captureWebVitals?: boolean;
45
+ /**
46
+ * A `pageview` event on load and on every SPA route change (history push/replace/popstate),
47
+ * carrying the same per-tab `sessionId` as every other event. This is what a visitor-count or
48
+ * funnel view in the Midline dashboard is built from. Default true.
49
+ */
50
+ capturePageviews?: boolean;
45
51
 
46
52
  /**
47
53
  * Requests that get a W3C `traceparent` header, so the backend's midline-agent
@@ -81,7 +87,7 @@ export interface MidlineBrowserConfig {
81
87
 
82
88
  /** An event as it is sent to the ingest API. */
83
89
  export interface BrowserEvent {
84
- eventType: "error" | "request" | "console" | "performance" | "custom";
90
+ eventType: "error" | "request" | "console" | "performance" | "pageview" | "custom";
85
91
  route: string;
86
92
  method?: string;
87
93
  statusCode?: number;
@@ -95,6 +101,8 @@ export interface BrowserEvent {
95
101
  userAgent?: string;
96
102
  traceId?: string;
97
103
  spanId?: string;
104
+ /** Stable for the tab's lifetime (sessionStorage-backed). Same value on every event this SDK sends. */
105
+ sessionId?: string;
98
106
  payload?: Record<string, unknown>;
99
107
  metadata: Record<string, unknown>;
100
108
  }
@@ -1,2 +1,2 @@
1
1
  /** Kept in step with package.json by test/browser.test.js; a browser bundle can't read package.json. */
2
- export const BROWSER_SDK_VERSION = "0.4.0";
2
+ export const BROWSER_SDK_VERSION = "0.5.0";
@@ -75,7 +75,7 @@ function uncaught(win, error, extra = {}) {
75
75
 
76
76
  async function withSdk(config, run, windowOptions) {
77
77
  const env = fakeWindow(windowOptions);
78
- Midline.init({ apiKey: KEY, flushIntervalMs: 60_000, captureWebVitals: false, ...config });
78
+ Midline.init({ apiKey: KEY, flushIntervalMs: 60_000, captureWebVitals: false, capturePageviews: false, ...config });
79
79
  try {
80
80
  await run(env);
81
81
  } finally {
@@ -298,7 +298,7 @@ test("browser: navigation starts a new trace; close restores everything it wrapp
298
298
  const originalPush = win.history.pushState;
299
299
  const originalError = win.console.error;
300
300
 
301
- Midline.init({ apiKey: KEY, flushIntervalMs: 60_000, captureWebVitals: false, captureConsole: true });
301
+ Midline.init({ apiKey: KEY, flushIntervalMs: 60_000, captureWebVitals: false, capturePageviews: false, captureConsole: true });
302
302
  assert.notEqual(win.fetch, originalFetch);
303
303
 
304
304
  Midline.captureMessage("before");
@@ -316,6 +316,52 @@ test("browser: navigation starts a new trace; close restores everything it wrapp
316
316
  delete globalThis.window;
317
317
  });
318
318
 
319
+ test("browser: pageviews fire on load and on SPA navigation, sharing one sessionId", async () => {
320
+ const env = fakeWindow();
321
+ const { win } = env;
322
+ win.document.referrer = "https://google.com/search";
323
+
324
+ Midline.init({ apiKey: KEY, flushIntervalMs: 60_000, captureWebVitals: false });
325
+ win.history.pushState({}, "", "/pricing");
326
+ win.history.replaceState({}, "", "/pricing/annual");
327
+ await Midline.flush();
328
+
329
+ const [initial, pushed, replaced] = env.sent();
330
+ assert.equal(env.sent().length, 3);
331
+ assert.equal(initial.eventType, "pageview");
332
+ assert.equal(initial.route, "/checkout");
333
+ assert.equal(initial.payload.referrer, "https://google.com/search");
334
+ assert.equal(pushed.eventType, "pageview");
335
+ assert.equal(pushed.route, "/pricing");
336
+ assert.equal(pushed.payload, undefined); // only the initial load carries a referrer
337
+ assert.equal(replaced.eventType, "pageview");
338
+ assert.equal(replaced.route, "/pricing/annual");
339
+
340
+ // One session for the whole tab, and every event — not just pageviews — carries it.
341
+ assert.match(initial.sessionId, /^[a-f0-9]{16,64}$/);
342
+ assert.ok([initial, pushed, replaced].every((event) => event.sessionId === initial.sessionId));
343
+
344
+ await Midline.close();
345
+ delete globalThis.window;
346
+ });
347
+
348
+ test("browser: capturePageviews: false suppresses pageviews without disabling other capture", async () => {
349
+ const env = fakeWindow();
350
+ const { win } = env;
351
+ Midline.init({ apiKey: KEY, flushIntervalMs: 60_000, captureWebVitals: false, capturePageviews: false });
352
+ win.history.pushState({}, "", "/pricing");
353
+ Midline.captureMessage("still works");
354
+ await Midline.flush();
355
+
356
+ const events = env.sent();
357
+ assert.ok(events.every((event) => event.eventType !== "pageview"));
358
+ assert.equal(events.length, 1);
359
+ assert.equal(events[0].payload.message, "still works");
360
+
361
+ await Midline.close();
362
+ delete globalThis.window;
363
+ });
364
+
319
365
  test("browser: pending events go out with keepalive when the page is hidden", async () => {
320
366
  await withSdk({}, async ({ win, calls }) => {
321
367
  Midline.captureMessage("leaving");