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 +25 -0
- package/README.md +9 -6
- package/dist/browser/client.d.ts +1 -0
- package/dist/browser/client.js +16 -2
- package/dist/browser/types.d.ts +9 -1
- package/dist/browser/version.d.ts +1 -1
- package/dist/browser/version.js +1 -1
- package/dist/esm/browser/client.js +16 -2
- package/dist/esm/browser/version.js +1 -1
- package/package.json +2 -2
- package/src/browser/client.ts +16 -2
- package/src/browser/types.ts +9 -1
- package/src/browser/version.ts +1 -1
- package/test/browser.test.js +48 -2
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)
|
|
382
|
-
|
|
383
|
-
Next.js or no framework at all, because it
|
|
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.
|
|
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
|
|
package/dist/browser/client.d.ts
CHANGED
package/dist/browser/client.js
CHANGED
|
@@ -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
|
}
|
package/dist/browser/types.d.ts
CHANGED
|
@@ -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.
|
|
2
|
+
export declare const BROWSER_SDK_VERSION = "0.5.0";
|
package/dist/browser/version.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
"description": "Midline — request, error and security monitoring for Node, and error, network
|
|
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": {
|
package/src/browser/client.ts
CHANGED
|
@@ -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
|
}
|
package/src/browser/types.ts
CHANGED
|
@@ -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
|
}
|
package/src/browser/version.ts
CHANGED
|
@@ -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.
|
|
2
|
+
export const BROWSER_SDK_VERSION = "0.5.0";
|
package/test/browser.test.js
CHANGED
|
@@ -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");
|