@testrelic/playwright-analytics 1.0.0 → 1.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/cli.cjs CHANGED
@@ -78,14 +78,16 @@ function recalculateSummary(reports) {
78
78
  let failed = 0;
79
79
  let flaky = 0;
80
80
  let skipped = 0;
81
+ let timedout = 0;
81
82
  for (const report of reports) {
82
83
  total += report.summary.total;
83
84
  passed += report.summary.passed;
84
85
  failed += report.summary.failed;
85
86
  flaky += report.summary.flaky;
86
87
  skipped += report.summary.skipped;
88
+ timedout += report.summary.timedout ?? 0;
87
89
  }
88
- return { total, passed, failed, flaky, skipped };
90
+ return { total, passed, failed, flaky, skipped, timedout };
89
91
  }
90
92
 
91
93
  // src/cli.ts
package/dist/fixture.cjs CHANGED
@@ -49,6 +49,7 @@ var NavigationTracker = class {
49
49
  this.records[this.records.length - 1].networkStats = {
50
50
  totalRequests: this.currentNetworkCounter.totalRequests,
51
51
  failedRequests: this.currentNetworkCounter.failedRequests,
52
+ failedRequestUrls: [...this.currentNetworkCounter.failedRequestUrls],
52
53
  totalBytes: this.currentNetworkCounter.totalBytes,
53
54
  byType: { ...this.currentNetworkCounter.byType }
54
55
  };
@@ -163,6 +164,7 @@ var NavigationTracker = class {
163
164
  const status = resp.status();
164
165
  if (status >= 400) {
165
166
  this.currentNetworkCounter.failedRequests++;
167
+ this.currentNetworkCounter.failedRequestUrls.push(status + " " + resp.url());
166
168
  }
167
169
  const contentLength = resp.headers()["content-length"];
168
170
  if (contentLength) {
@@ -176,9 +178,14 @@ var NavigationTracker = class {
176
178
  };
177
179
  this.page.on("response", onResponse);
178
180
  this.listeners.push({ event: "response", handler: onResponse });
179
- const onRequestFailed = () => {
181
+ const onRequestFailed = (request) => {
180
182
  if (this.currentNetworkCounter) {
181
183
  this.currentNetworkCounter.failedRequests++;
184
+ try {
185
+ const req = request;
186
+ this.currentNetworkCounter.failedRequestUrls.push("ERR " + req.url());
187
+ } catch {
188
+ }
182
189
  }
183
190
  };
184
191
  this.page.on("requestfailed", onRequestFailed);
@@ -225,6 +232,7 @@ var NavigationTracker = class {
225
232
  this.records[this.records.length - 1].networkStats = {
226
233
  totalRequests: this.currentNetworkCounter.totalRequests,
227
234
  failedRequests: this.currentNetworkCounter.failedRequests,
235
+ failedRequestUrls: [...this.currentNetworkCounter.failedRequestUrls],
228
236
  totalBytes: this.currentNetworkCounter.totalBytes,
229
237
  byType: { ...this.currentNetworkCounter.byType }
230
238
  };
@@ -242,6 +250,7 @@ var NavigationTracker = class {
242
250
  return {
243
251
  totalRequests: 0,
244
252
  failedRequests: 0,
253
+ failedRequestUrls: [],
245
254
  totalBytes: 0,
246
255
  byType: { xhr: 0, document: 0, script: 0, stylesheet: 0, image: 0, font: 0, other: 0 }
247
256
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fixture.ts","../src/navigation-tracker.ts"],"sourcesContent":["/**\n * @testrelic/playwright-analytics/fixture\n *\n * Extended Playwright test fixture that wraps the default `page` fixture\n * to automatically track navigation events.\n */\n\nimport { test as base, expect } from '@playwright/test';\nimport { NavigationTracker } from './navigation-tracker.js';\n\nexport { expect };\n\nexport const test = base.extend({\n page: async ({ page }, use, testInfo) => {\n const tracker = new NavigationTracker(page as never);\n try {\n await tracker.init();\n } catch {\n // Graceful degradation: continue without SPA detection\n }\n\n await use(page);\n\n try {\n tracker.flush(testInfo);\n } catch {\n // FR-009: fixture without reporter is harmless\n }\n tracker.dispose();\n },\n});\n","/**\n * NavigationTracker — wraps a Playwright Page to track navigation events.\n *\n * Intercepts page methods (goto, goBack, goForward, reload) and listens\n * for DOM events (framenavigated, domcontentloaded, load) plus injected\n * SPA detection scripts.\n */\n\nimport type { NavigationAnnotation, NavigationType, NetworkStats, ResourceBreakdown } from '@testrelic/core';\n\n// Minimal Playwright Page type subset for loose coupling\ninterface PageLike {\n goto(url: string, options?: unknown): Promise<unknown>;\n goBack(options?: unknown): Promise<unknown>;\n goForward(options?: unknown): Promise<unknown>;\n reload(options?: unknown): Promise<unknown>;\n url(): string;\n on(event: string, handler: (...args: unknown[]) => void): void;\n off(event: string, handler: (...args: unknown[]) => void): void;\n addInitScript(script: string | { path?: string } | (() => void)): Promise<void>;\n evaluate(fn: (...args: unknown[]) => unknown, ...args: unknown[]): Promise<unknown>;\n mainFrame(): { url(): string };\n}\n\ninterface TestInfoLike {\n annotations: Array<{ type: string; description?: string }>;\n}\n\ninterface NavigationRecord {\n url: string;\n navigationType: NavigationType;\n timestamp: string;\n domContentLoadedAt?: string;\n networkIdleAt?: string;\n networkStats?: NetworkStats;\n}\n\ninterface NetworkCounter {\n totalRequests: number;\n failedRequests: number;\n totalBytes: number;\n byType: Record<keyof ResourceBreakdown, number>;\n}\n\nexport class NavigationTracker {\n private records: NavigationRecord[] = [];\n private listeners: Array<{ event: string; handler: (...args: unknown[]) => void }> = [];\n private origGoto: PageLike['goto'];\n private origGoBack: PageLike['goBack'];\n private origGoForward: PageLike['goForward'];\n private origReload: PageLike['reload'];\n private lastDomContentLoaded: string | undefined;\n private includeNetworkStats: boolean;\n private currentNetworkCounter: NetworkCounter | null = null;\n\n constructor(private page: PageLike, options?: { includeNetworkStats?: boolean }) {\n this.includeNetworkStats = options?.includeNetworkStats ?? true;\n // Store originals\n this.origGoto = page.goto.bind(page);\n this.origGoBack = page.goBack.bind(page);\n this.origGoForward = page.goForward.bind(page);\n this.origReload = page.reload.bind(page);\n\n this.interceptMethods();\n this.attachListeners();\n }\n\n async init(): Promise<void> {\n await this.injectSPADetection();\n }\n\n flush(testInfo: TestInfoLike): void {\n // Finalize network stats for the last navigation\n if (this.includeNetworkStats && this.currentNetworkCounter && this.records.length > 0) {\n this.records[this.records.length - 1].networkStats = {\n totalRequests: this.currentNetworkCounter.totalRequests,\n failedRequests: this.currentNetworkCounter.failedRequests,\n totalBytes: this.currentNetworkCounter.totalBytes,\n byType: { ...this.currentNetworkCounter.byType },\n };\n }\n\n for (const record of this.records) {\n const annotation: NavigationAnnotation = {\n url: record.url,\n navigationType: record.navigationType,\n timestamp: record.timestamp,\n domContentLoadedAt: record.domContentLoadedAt,\n networkIdleAt: record.networkIdleAt,\n networkStats: record.networkStats,\n };\n testInfo.annotations.push({\n type: 'lambdatest-navigation',\n description: JSON.stringify(annotation),\n });\n }\n }\n\n dispose(): void {\n // Restore original methods\n (this.page as Record<string, unknown>).goto = this.origGoto;\n (this.page as Record<string, unknown>).goBack = this.origGoBack;\n (this.page as Record<string, unknown>).goForward = this.origGoForward;\n (this.page as Record<string, unknown>).reload = this.origReload;\n\n // Remove event listeners\n for (const { event, handler } of this.listeners) {\n this.page.off(event, handler);\n }\n this.listeners = [];\n this.records = [];\n }\n\n getRecords(): readonly NavigationRecord[] {\n return this.records;\n }\n\n private interceptMethods(): void {\n const self = this;\n const page = this.page;\n\n (page as Record<string, unknown>).goto = async function (url: string, options?: unknown) {\n self.recordNavigation(url, 'goto');\n return self.origGoto(url, options);\n };\n\n (page as Record<string, unknown>).goBack = async function (options?: unknown) {\n const result = await self.origGoBack(options);\n self.recordNavigation(page.url(), 'back');\n return result;\n };\n\n (page as Record<string, unknown>).goForward = async function (options?: unknown) {\n const result = await self.origGoForward(options);\n self.recordNavigation(page.url(), 'forward');\n return result;\n };\n\n (page as Record<string, unknown>).reload = async function (options?: unknown) {\n self.recordNavigation(page.url(), 'refresh');\n return self.origReload(options);\n };\n }\n\n private attachListeners(): void {\n // Track DOMContentLoaded for lifecycle timestamps\n const onDomContentLoaded = () => {\n this.lastDomContentLoaded = new Date().toISOString();\n if (this.records.length > 0) {\n this.records[this.records.length - 1].domContentLoadedAt = this.lastDomContentLoaded;\n }\n };\n this.page.on('domcontentloaded', onDomContentLoaded as (...args: unknown[]) => void);\n this.listeners.push({ event: 'domcontentloaded', handler: onDomContentLoaded as (...args: unknown[]) => void });\n\n // Track main frame navigation for non-intercepted navigations (link clicks, form submits)\n const onFrameNavigated = (frame: unknown) => {\n // Only track main frame\n try {\n const frameObj = frame as { url(): string; parentFrame(): unknown };\n if (typeof frameObj.parentFrame === 'function' && frameObj.parentFrame() !== null) {\n return; // Skip sub-frames\n }\n const url = frameObj.url();\n // Skip if we already recorded this URL in the last 50ms (from method interception)\n const lastRecord = this.records[this.records.length - 1];\n if (lastRecord) {\n const timeDiff = Date.now() - new Date(lastRecord.timestamp).getTime();\n if (timeDiff < 50 && lastRecord.url === url) {\n return; // Duplicate from method interception\n }\n }\n // This is an untracked navigation (link click, form submit, etc.)\n this.recordNavigation(url, 'navigation');\n } catch {\n // Ignore frame navigation errors\n }\n };\n this.page.on('framenavigated', onFrameNavigated);\n this.listeners.push({ event: 'framenavigated', handler: onFrameNavigated });\n\n // Track console messages from injected SPA detection script\n const onConsoleMessage = (msg: unknown) => {\n try {\n const msgObj = msg as { text(): string; type(): string };\n if (msgObj.type() !== 'debug') return;\n const text = msgObj.text();\n if (!text.startsWith('__testrelic_nav:')) return;\n const data = JSON.parse(text.slice('__testrelic_nav:'.length));\n if (data.type && data.url) {\n this.recordNavigation(data.url, data.type as NavigationType);\n }\n } catch {\n // Ignore parse errors\n }\n };\n this.page.on('console', onConsoleMessage);\n this.listeners.push({ event: 'console', handler: onConsoleMessage });\n\n // Network stats tracking\n if (this.includeNetworkStats) {\n const onRequest = () => {\n if (this.currentNetworkCounter) {\n this.currentNetworkCounter.totalRequests++;\n }\n };\n this.page.on('request', onRequest);\n this.listeners.push({ event: 'request', handler: onRequest });\n\n const onResponse = (response: unknown) => {\n if (!this.currentNetworkCounter) return;\n try {\n const resp = response as {\n status(): number;\n headers(): Record<string, string>;\n request(): { resourceType(): string };\n };\n const status = resp.status();\n if (status >= 400) {\n this.currentNetworkCounter.failedRequests++;\n }\n const contentLength = resp.headers()['content-length'];\n if (contentLength) {\n this.currentNetworkCounter.totalBytes += parseInt(contentLength, 10) || 0;\n }\n const resourceType = resp.request().resourceType();\n const typeKey = this.mapResourceType(resourceType);\n this.currentNetworkCounter.byType[typeKey]++;\n } catch {\n // Ignore response processing errors\n }\n };\n this.page.on('response', onResponse);\n this.listeners.push({ event: 'response', handler: onResponse });\n\n const onRequestFailed = () => {\n if (this.currentNetworkCounter) {\n this.currentNetworkCounter.failedRequests++;\n }\n };\n this.page.on('requestfailed', onRequestFailed);\n this.listeners.push({ event: 'requestfailed', handler: onRequestFailed });\n }\n }\n\n private async injectSPADetection(): Promise<void> {\n try {\n await this.page.addInitScript(() => {\n const origPush = history.pushState.bind(history);\n const origReplace = history.replaceState.bind(history);\n\n history.pushState = function (...args: Parameters<typeof origPush>) {\n origPush(...args);\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'spa_route',\n url: location.href,\n }));\n };\n\n history.replaceState = function (...args: Parameters<typeof origReplace>) {\n origReplace(...args);\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'spa_replace',\n url: location.href,\n }));\n };\n\n window.addEventListener('popstate', () => {\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'popstate',\n url: location.href,\n }));\n });\n\n window.addEventListener('hashchange', () => {\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'hash_change',\n url: location.href,\n }));\n });\n });\n } catch {\n // Ignore injection errors (page may be closed)\n }\n }\n\n private recordNavigation(url: string, type: NavigationType): void {\n // Finalize network stats for the previous navigation\n if (this.includeNetworkStats && this.currentNetworkCounter && this.records.length > 0) {\n this.records[this.records.length - 1].networkStats = {\n totalRequests: this.currentNetworkCounter.totalRequests,\n failedRequests: this.currentNetworkCounter.failedRequests,\n totalBytes: this.currentNetworkCounter.totalBytes,\n byType: { ...this.currentNetworkCounter.byType },\n };\n }\n\n this.records.push({\n url,\n navigationType: type,\n timestamp: new Date().toISOString(),\n });\n\n // Start fresh counter for this navigation\n if (this.includeNetworkStats) {\n this.currentNetworkCounter = this.createNetworkCounter();\n }\n }\n\n private createNetworkCounter(): NetworkCounter {\n return {\n totalRequests: 0,\n failedRequests: 0,\n totalBytes: 0,\n byType: { xhr: 0, document: 0, script: 0, stylesheet: 0, image: 0, font: 0, other: 0 },\n };\n }\n\n private mapResourceType(type: string): keyof ResourceBreakdown {\n switch (type) {\n case 'xhr':\n case 'fetch':\n return 'xhr';\n case 'document':\n return 'document';\n case 'script':\n return 'script';\n case 'stylesheet':\n return 'stylesheet';\n case 'image':\n return 'image';\n case 'font':\n return 'font';\n default:\n return 'other';\n }\n }\n}\n"],"mappings":"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","names":["base"]}
1
+ {"version":3,"sources":["../src/fixture.ts","../src/navigation-tracker.ts"],"sourcesContent":["/**\n * @testrelic/playwright-analytics/fixture\n *\n * Extended Playwright test fixture that wraps the default `page` fixture\n * to automatically track navigation events.\n */\n\nimport { test as base, expect } from '@playwright/test';\nimport { NavigationTracker } from './navigation-tracker.js';\n\nexport { expect };\n\nexport const test = base.extend({\n page: async ({ page }, use, testInfo) => {\n const tracker = new NavigationTracker(page as never);\n try {\n await tracker.init();\n } catch {\n // Graceful degradation: continue without SPA detection\n }\n\n await use(page);\n\n try {\n tracker.flush(testInfo);\n } catch {\n // FR-009: fixture without reporter is harmless\n }\n tracker.dispose();\n },\n});\n","/**\n * NavigationTracker — wraps a Playwright Page to track navigation events.\n *\n * Intercepts page methods (goto, goBack, goForward, reload) and listens\n * for DOM events (framenavigated, domcontentloaded, load) plus injected\n * SPA detection scripts.\n */\n\nimport type { NavigationAnnotation, NavigationType, NetworkStats, ResourceBreakdown } from '@testrelic/core';\n\n// Minimal Playwright Page type subset for loose coupling\ninterface PageLike {\n goto(url: string, options?: unknown): Promise<unknown>;\n goBack(options?: unknown): Promise<unknown>;\n goForward(options?: unknown): Promise<unknown>;\n reload(options?: unknown): Promise<unknown>;\n url(): string;\n on(event: string, handler: (...args: unknown[]) => void): void;\n off(event: string, handler: (...args: unknown[]) => void): void;\n addInitScript(script: string | { path?: string } | (() => void)): Promise<void>;\n evaluate(fn: (...args: unknown[]) => unknown, ...args: unknown[]): Promise<unknown>;\n mainFrame(): { url(): string };\n}\n\ninterface TestInfoLike {\n annotations: Array<{ type: string; description?: string }>;\n}\n\ninterface NavigationRecord {\n url: string;\n navigationType: NavigationType;\n timestamp: string;\n domContentLoadedAt?: string;\n networkIdleAt?: string;\n networkStats?: NetworkStats;\n}\n\ninterface NetworkCounter {\n totalRequests: number;\n failedRequests: number;\n failedRequestUrls: string[];\n totalBytes: number;\n byType: Record<keyof ResourceBreakdown, number>;\n}\n\nexport class NavigationTracker {\n private records: NavigationRecord[] = [];\n private listeners: Array<{ event: string; handler: (...args: unknown[]) => void }> = [];\n private origGoto: PageLike['goto'];\n private origGoBack: PageLike['goBack'];\n private origGoForward: PageLike['goForward'];\n private origReload: PageLike['reload'];\n private lastDomContentLoaded: string | undefined;\n private includeNetworkStats: boolean;\n private currentNetworkCounter: NetworkCounter | null = null;\n\n constructor(private page: PageLike, options?: { includeNetworkStats?: boolean }) {\n this.includeNetworkStats = options?.includeNetworkStats ?? true;\n // Store originals\n this.origGoto = page.goto.bind(page);\n this.origGoBack = page.goBack.bind(page);\n this.origGoForward = page.goForward.bind(page);\n this.origReload = page.reload.bind(page);\n\n this.interceptMethods();\n this.attachListeners();\n }\n\n async init(): Promise<void> {\n await this.injectSPADetection();\n }\n\n flush(testInfo: TestInfoLike): void {\n // Finalize network stats for the last navigation\n if (this.includeNetworkStats && this.currentNetworkCounter && this.records.length > 0) {\n this.records[this.records.length - 1].networkStats = {\n totalRequests: this.currentNetworkCounter.totalRequests,\n failedRequests: this.currentNetworkCounter.failedRequests,\n failedRequestUrls: [...this.currentNetworkCounter.failedRequestUrls],\n totalBytes: this.currentNetworkCounter.totalBytes,\n byType: { ...this.currentNetworkCounter.byType },\n };\n }\n\n for (const record of this.records) {\n const annotation: NavigationAnnotation = {\n url: record.url,\n navigationType: record.navigationType,\n timestamp: record.timestamp,\n domContentLoadedAt: record.domContentLoadedAt,\n networkIdleAt: record.networkIdleAt,\n networkStats: record.networkStats,\n };\n testInfo.annotations.push({\n type: 'lambdatest-navigation',\n description: JSON.stringify(annotation),\n });\n }\n }\n\n dispose(): void {\n // Restore original methods\n (this.page as Record<string, unknown>).goto = this.origGoto;\n (this.page as Record<string, unknown>).goBack = this.origGoBack;\n (this.page as Record<string, unknown>).goForward = this.origGoForward;\n (this.page as Record<string, unknown>).reload = this.origReload;\n\n // Remove event listeners\n for (const { event, handler } of this.listeners) {\n this.page.off(event, handler);\n }\n this.listeners = [];\n this.records = [];\n }\n\n getRecords(): readonly NavigationRecord[] {\n return this.records;\n }\n\n private interceptMethods(): void {\n const self = this;\n const page = this.page;\n\n (page as Record<string, unknown>).goto = async function (url: string, options?: unknown) {\n self.recordNavigation(url, 'goto');\n return self.origGoto(url, options);\n };\n\n (page as Record<string, unknown>).goBack = async function (options?: unknown) {\n const result = await self.origGoBack(options);\n self.recordNavigation(page.url(), 'back');\n return result;\n };\n\n (page as Record<string, unknown>).goForward = async function (options?: unknown) {\n const result = await self.origGoForward(options);\n self.recordNavigation(page.url(), 'forward');\n return result;\n };\n\n (page as Record<string, unknown>).reload = async function (options?: unknown) {\n self.recordNavigation(page.url(), 'refresh');\n return self.origReload(options);\n };\n }\n\n private attachListeners(): void {\n // Track DOMContentLoaded for lifecycle timestamps\n const onDomContentLoaded = () => {\n this.lastDomContentLoaded = new Date().toISOString();\n if (this.records.length > 0) {\n this.records[this.records.length - 1].domContentLoadedAt = this.lastDomContentLoaded;\n }\n };\n this.page.on('domcontentloaded', onDomContentLoaded as (...args: unknown[]) => void);\n this.listeners.push({ event: 'domcontentloaded', handler: onDomContentLoaded as (...args: unknown[]) => void });\n\n // Track main frame navigation for non-intercepted navigations (link clicks, form submits)\n const onFrameNavigated = (frame: unknown) => {\n // Only track main frame\n try {\n const frameObj = frame as { url(): string; parentFrame(): unknown };\n if (typeof frameObj.parentFrame === 'function' && frameObj.parentFrame() !== null) {\n return; // Skip sub-frames\n }\n const url = frameObj.url();\n // Skip if we already recorded this URL in the last 50ms (from method interception)\n const lastRecord = this.records[this.records.length - 1];\n if (lastRecord) {\n const timeDiff = Date.now() - new Date(lastRecord.timestamp).getTime();\n if (timeDiff < 50 && lastRecord.url === url) {\n return; // Duplicate from method interception\n }\n }\n // This is an untracked navigation (link click, form submit, etc.)\n this.recordNavigation(url, 'navigation');\n } catch {\n // Ignore frame navigation errors\n }\n };\n this.page.on('framenavigated', onFrameNavigated);\n this.listeners.push({ event: 'framenavigated', handler: onFrameNavigated });\n\n // Track console messages from injected SPA detection script\n const onConsoleMessage = (msg: unknown) => {\n try {\n const msgObj = msg as { text(): string; type(): string };\n if (msgObj.type() !== 'debug') return;\n const text = msgObj.text();\n if (!text.startsWith('__testrelic_nav:')) return;\n const data = JSON.parse(text.slice('__testrelic_nav:'.length));\n if (data.type && data.url) {\n this.recordNavigation(data.url, data.type as NavigationType);\n }\n } catch {\n // Ignore parse errors\n }\n };\n this.page.on('console', onConsoleMessage);\n this.listeners.push({ event: 'console', handler: onConsoleMessage });\n\n // Network stats tracking\n if (this.includeNetworkStats) {\n const onRequest = () => {\n if (this.currentNetworkCounter) {\n this.currentNetworkCounter.totalRequests++;\n }\n };\n this.page.on('request', onRequest);\n this.listeners.push({ event: 'request', handler: onRequest });\n\n const onResponse = (response: unknown) => {\n if (!this.currentNetworkCounter) return;\n try {\n const resp = response as {\n url(): string;\n status(): number;\n headers(): Record<string, string>;\n request(): { resourceType(): string };\n };\n const status = resp.status();\n if (status >= 400) {\n this.currentNetworkCounter.failedRequests++;\n this.currentNetworkCounter.failedRequestUrls.push(status + ' ' + resp.url());\n }\n const contentLength = resp.headers()['content-length'];\n if (contentLength) {\n this.currentNetworkCounter.totalBytes += parseInt(contentLength, 10) || 0;\n }\n const resourceType = resp.request().resourceType();\n const typeKey = this.mapResourceType(resourceType);\n this.currentNetworkCounter.byType[typeKey]++;\n } catch {\n // Ignore response processing errors\n }\n };\n this.page.on('response', onResponse);\n this.listeners.push({ event: 'response', handler: onResponse });\n\n const onRequestFailed = (request: unknown) => {\n if (this.currentNetworkCounter) {\n this.currentNetworkCounter.failedRequests++;\n try {\n const req = request as { url(): string };\n this.currentNetworkCounter.failedRequestUrls.push('ERR ' + req.url());\n } catch {\n // Ignore\n }\n }\n };\n this.page.on('requestfailed', onRequestFailed);\n this.listeners.push({ event: 'requestfailed', handler: onRequestFailed });\n }\n }\n\n private async injectSPADetection(): Promise<void> {\n try {\n await this.page.addInitScript(() => {\n const origPush = history.pushState.bind(history);\n const origReplace = history.replaceState.bind(history);\n\n history.pushState = function (...args: Parameters<typeof origPush>) {\n origPush(...args);\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'spa_route',\n url: location.href,\n }));\n };\n\n history.replaceState = function (...args: Parameters<typeof origReplace>) {\n origReplace(...args);\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'spa_replace',\n url: location.href,\n }));\n };\n\n window.addEventListener('popstate', () => {\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'popstate',\n url: location.href,\n }));\n });\n\n window.addEventListener('hashchange', () => {\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'hash_change',\n url: location.href,\n }));\n });\n });\n } catch {\n // Ignore injection errors (page may be closed)\n }\n }\n\n private recordNavigation(url: string, type: NavigationType): void {\n // Finalize network stats for the previous navigation\n if (this.includeNetworkStats && this.currentNetworkCounter && this.records.length > 0) {\n this.records[this.records.length - 1].networkStats = {\n totalRequests: this.currentNetworkCounter.totalRequests,\n failedRequests: this.currentNetworkCounter.failedRequests,\n failedRequestUrls: [...this.currentNetworkCounter.failedRequestUrls],\n totalBytes: this.currentNetworkCounter.totalBytes,\n byType: { ...this.currentNetworkCounter.byType },\n };\n }\n\n this.records.push({\n url,\n navigationType: type,\n timestamp: new Date().toISOString(),\n });\n\n // Start fresh counter for this navigation\n if (this.includeNetworkStats) {\n this.currentNetworkCounter = this.createNetworkCounter();\n }\n }\n\n private createNetworkCounter(): NetworkCounter {\n return {\n totalRequests: 0,\n failedRequests: 0,\n failedRequestUrls: [],\n totalBytes: 0,\n byType: { xhr: 0, document: 0, script: 0, stylesheet: 0, image: 0, font: 0, other: 0 },\n };\n }\n\n private mapResourceType(type: string): keyof ResourceBreakdown {\n switch (type) {\n case 'xhr':\n case 'fetch':\n return 'xhr';\n case 'document':\n return 'document';\n case 'script':\n return 'script';\n case 'stylesheet':\n return 'stylesheet';\n case 'image':\n return 'image';\n case 'font':\n return 'font';\n default:\n return 'other';\n }\n }\n}\n"],"mappings":"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","names":["base"]}
package/dist/fixture.js CHANGED
@@ -24,6 +24,7 @@ var NavigationTracker = class {
24
24
  this.records[this.records.length - 1].networkStats = {
25
25
  totalRequests: this.currentNetworkCounter.totalRequests,
26
26
  failedRequests: this.currentNetworkCounter.failedRequests,
27
+ failedRequestUrls: [...this.currentNetworkCounter.failedRequestUrls],
27
28
  totalBytes: this.currentNetworkCounter.totalBytes,
28
29
  byType: { ...this.currentNetworkCounter.byType }
29
30
  };
@@ -138,6 +139,7 @@ var NavigationTracker = class {
138
139
  const status = resp.status();
139
140
  if (status >= 400) {
140
141
  this.currentNetworkCounter.failedRequests++;
142
+ this.currentNetworkCounter.failedRequestUrls.push(status + " " + resp.url());
141
143
  }
142
144
  const contentLength = resp.headers()["content-length"];
143
145
  if (contentLength) {
@@ -151,9 +153,14 @@ var NavigationTracker = class {
151
153
  };
152
154
  this.page.on("response", onResponse);
153
155
  this.listeners.push({ event: "response", handler: onResponse });
154
- const onRequestFailed = () => {
156
+ const onRequestFailed = (request) => {
155
157
  if (this.currentNetworkCounter) {
156
158
  this.currentNetworkCounter.failedRequests++;
159
+ try {
160
+ const req = request;
161
+ this.currentNetworkCounter.failedRequestUrls.push("ERR " + req.url());
162
+ } catch {
163
+ }
157
164
  }
158
165
  };
159
166
  this.page.on("requestfailed", onRequestFailed);
@@ -200,6 +207,7 @@ var NavigationTracker = class {
200
207
  this.records[this.records.length - 1].networkStats = {
201
208
  totalRequests: this.currentNetworkCounter.totalRequests,
202
209
  failedRequests: this.currentNetworkCounter.failedRequests,
210
+ failedRequestUrls: [...this.currentNetworkCounter.failedRequestUrls],
203
211
  totalBytes: this.currentNetworkCounter.totalBytes,
204
212
  byType: { ...this.currentNetworkCounter.byType }
205
213
  };
@@ -217,6 +225,7 @@ var NavigationTracker = class {
217
225
  return {
218
226
  totalRequests: 0,
219
227
  failedRequests: 0,
228
+ failedRequestUrls: [],
220
229
  totalBytes: 0,
221
230
  byType: { xhr: 0, document: 0, script: 0, stylesheet: 0, image: 0, font: 0, other: 0 }
222
231
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fixture.ts","../src/navigation-tracker.ts"],"sourcesContent":["/**\n * @testrelic/playwright-analytics/fixture\n *\n * Extended Playwright test fixture that wraps the default `page` fixture\n * to automatically track navigation events.\n */\n\nimport { test as base, expect } from '@playwright/test';\nimport { NavigationTracker } from './navigation-tracker.js';\n\nexport { expect };\n\nexport const test = base.extend({\n page: async ({ page }, use, testInfo) => {\n const tracker = new NavigationTracker(page as never);\n try {\n await tracker.init();\n } catch {\n // Graceful degradation: continue without SPA detection\n }\n\n await use(page);\n\n try {\n tracker.flush(testInfo);\n } catch {\n // FR-009: fixture without reporter is harmless\n }\n tracker.dispose();\n },\n});\n","/**\n * NavigationTracker — wraps a Playwright Page to track navigation events.\n *\n * Intercepts page methods (goto, goBack, goForward, reload) and listens\n * for DOM events (framenavigated, domcontentloaded, load) plus injected\n * SPA detection scripts.\n */\n\nimport type { NavigationAnnotation, NavigationType, NetworkStats, ResourceBreakdown } from '@testrelic/core';\n\n// Minimal Playwright Page type subset for loose coupling\ninterface PageLike {\n goto(url: string, options?: unknown): Promise<unknown>;\n goBack(options?: unknown): Promise<unknown>;\n goForward(options?: unknown): Promise<unknown>;\n reload(options?: unknown): Promise<unknown>;\n url(): string;\n on(event: string, handler: (...args: unknown[]) => void): void;\n off(event: string, handler: (...args: unknown[]) => void): void;\n addInitScript(script: string | { path?: string } | (() => void)): Promise<void>;\n evaluate(fn: (...args: unknown[]) => unknown, ...args: unknown[]): Promise<unknown>;\n mainFrame(): { url(): string };\n}\n\ninterface TestInfoLike {\n annotations: Array<{ type: string; description?: string }>;\n}\n\ninterface NavigationRecord {\n url: string;\n navigationType: NavigationType;\n timestamp: string;\n domContentLoadedAt?: string;\n networkIdleAt?: string;\n networkStats?: NetworkStats;\n}\n\ninterface NetworkCounter {\n totalRequests: number;\n failedRequests: number;\n totalBytes: number;\n byType: Record<keyof ResourceBreakdown, number>;\n}\n\nexport class NavigationTracker {\n private records: NavigationRecord[] = [];\n private listeners: Array<{ event: string; handler: (...args: unknown[]) => void }> = [];\n private origGoto: PageLike['goto'];\n private origGoBack: PageLike['goBack'];\n private origGoForward: PageLike['goForward'];\n private origReload: PageLike['reload'];\n private lastDomContentLoaded: string | undefined;\n private includeNetworkStats: boolean;\n private currentNetworkCounter: NetworkCounter | null = null;\n\n constructor(private page: PageLike, options?: { includeNetworkStats?: boolean }) {\n this.includeNetworkStats = options?.includeNetworkStats ?? true;\n // Store originals\n this.origGoto = page.goto.bind(page);\n this.origGoBack = page.goBack.bind(page);\n this.origGoForward = page.goForward.bind(page);\n this.origReload = page.reload.bind(page);\n\n this.interceptMethods();\n this.attachListeners();\n }\n\n async init(): Promise<void> {\n await this.injectSPADetection();\n }\n\n flush(testInfo: TestInfoLike): void {\n // Finalize network stats for the last navigation\n if (this.includeNetworkStats && this.currentNetworkCounter && this.records.length > 0) {\n this.records[this.records.length - 1].networkStats = {\n totalRequests: this.currentNetworkCounter.totalRequests,\n failedRequests: this.currentNetworkCounter.failedRequests,\n totalBytes: this.currentNetworkCounter.totalBytes,\n byType: { ...this.currentNetworkCounter.byType },\n };\n }\n\n for (const record of this.records) {\n const annotation: NavigationAnnotation = {\n url: record.url,\n navigationType: record.navigationType,\n timestamp: record.timestamp,\n domContentLoadedAt: record.domContentLoadedAt,\n networkIdleAt: record.networkIdleAt,\n networkStats: record.networkStats,\n };\n testInfo.annotations.push({\n type: 'lambdatest-navigation',\n description: JSON.stringify(annotation),\n });\n }\n }\n\n dispose(): void {\n // Restore original methods\n (this.page as Record<string, unknown>).goto = this.origGoto;\n (this.page as Record<string, unknown>).goBack = this.origGoBack;\n (this.page as Record<string, unknown>).goForward = this.origGoForward;\n (this.page as Record<string, unknown>).reload = this.origReload;\n\n // Remove event listeners\n for (const { event, handler } of this.listeners) {\n this.page.off(event, handler);\n }\n this.listeners = [];\n this.records = [];\n }\n\n getRecords(): readonly NavigationRecord[] {\n return this.records;\n }\n\n private interceptMethods(): void {\n const self = this;\n const page = this.page;\n\n (page as Record<string, unknown>).goto = async function (url: string, options?: unknown) {\n self.recordNavigation(url, 'goto');\n return self.origGoto(url, options);\n };\n\n (page as Record<string, unknown>).goBack = async function (options?: unknown) {\n const result = await self.origGoBack(options);\n self.recordNavigation(page.url(), 'back');\n return result;\n };\n\n (page as Record<string, unknown>).goForward = async function (options?: unknown) {\n const result = await self.origGoForward(options);\n self.recordNavigation(page.url(), 'forward');\n return result;\n };\n\n (page as Record<string, unknown>).reload = async function (options?: unknown) {\n self.recordNavigation(page.url(), 'refresh');\n return self.origReload(options);\n };\n }\n\n private attachListeners(): void {\n // Track DOMContentLoaded for lifecycle timestamps\n const onDomContentLoaded = () => {\n this.lastDomContentLoaded = new Date().toISOString();\n if (this.records.length > 0) {\n this.records[this.records.length - 1].domContentLoadedAt = this.lastDomContentLoaded;\n }\n };\n this.page.on('domcontentloaded', onDomContentLoaded as (...args: unknown[]) => void);\n this.listeners.push({ event: 'domcontentloaded', handler: onDomContentLoaded as (...args: unknown[]) => void });\n\n // Track main frame navigation for non-intercepted navigations (link clicks, form submits)\n const onFrameNavigated = (frame: unknown) => {\n // Only track main frame\n try {\n const frameObj = frame as { url(): string; parentFrame(): unknown };\n if (typeof frameObj.parentFrame === 'function' && frameObj.parentFrame() !== null) {\n return; // Skip sub-frames\n }\n const url = frameObj.url();\n // Skip if we already recorded this URL in the last 50ms (from method interception)\n const lastRecord = this.records[this.records.length - 1];\n if (lastRecord) {\n const timeDiff = Date.now() - new Date(lastRecord.timestamp).getTime();\n if (timeDiff < 50 && lastRecord.url === url) {\n return; // Duplicate from method interception\n }\n }\n // This is an untracked navigation (link click, form submit, etc.)\n this.recordNavigation(url, 'navigation');\n } catch {\n // Ignore frame navigation errors\n }\n };\n this.page.on('framenavigated', onFrameNavigated);\n this.listeners.push({ event: 'framenavigated', handler: onFrameNavigated });\n\n // Track console messages from injected SPA detection script\n const onConsoleMessage = (msg: unknown) => {\n try {\n const msgObj = msg as { text(): string; type(): string };\n if (msgObj.type() !== 'debug') return;\n const text = msgObj.text();\n if (!text.startsWith('__testrelic_nav:')) return;\n const data = JSON.parse(text.slice('__testrelic_nav:'.length));\n if (data.type && data.url) {\n this.recordNavigation(data.url, data.type as NavigationType);\n }\n } catch {\n // Ignore parse errors\n }\n };\n this.page.on('console', onConsoleMessage);\n this.listeners.push({ event: 'console', handler: onConsoleMessage });\n\n // Network stats tracking\n if (this.includeNetworkStats) {\n const onRequest = () => {\n if (this.currentNetworkCounter) {\n this.currentNetworkCounter.totalRequests++;\n }\n };\n this.page.on('request', onRequest);\n this.listeners.push({ event: 'request', handler: onRequest });\n\n const onResponse = (response: unknown) => {\n if (!this.currentNetworkCounter) return;\n try {\n const resp = response as {\n status(): number;\n headers(): Record<string, string>;\n request(): { resourceType(): string };\n };\n const status = resp.status();\n if (status >= 400) {\n this.currentNetworkCounter.failedRequests++;\n }\n const contentLength = resp.headers()['content-length'];\n if (contentLength) {\n this.currentNetworkCounter.totalBytes += parseInt(contentLength, 10) || 0;\n }\n const resourceType = resp.request().resourceType();\n const typeKey = this.mapResourceType(resourceType);\n this.currentNetworkCounter.byType[typeKey]++;\n } catch {\n // Ignore response processing errors\n }\n };\n this.page.on('response', onResponse);\n this.listeners.push({ event: 'response', handler: onResponse });\n\n const onRequestFailed = () => {\n if (this.currentNetworkCounter) {\n this.currentNetworkCounter.failedRequests++;\n }\n };\n this.page.on('requestfailed', onRequestFailed);\n this.listeners.push({ event: 'requestfailed', handler: onRequestFailed });\n }\n }\n\n private async injectSPADetection(): Promise<void> {\n try {\n await this.page.addInitScript(() => {\n const origPush = history.pushState.bind(history);\n const origReplace = history.replaceState.bind(history);\n\n history.pushState = function (...args: Parameters<typeof origPush>) {\n origPush(...args);\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'spa_route',\n url: location.href,\n }));\n };\n\n history.replaceState = function (...args: Parameters<typeof origReplace>) {\n origReplace(...args);\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'spa_replace',\n url: location.href,\n }));\n };\n\n window.addEventListener('popstate', () => {\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'popstate',\n url: location.href,\n }));\n });\n\n window.addEventListener('hashchange', () => {\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'hash_change',\n url: location.href,\n }));\n });\n });\n } catch {\n // Ignore injection errors (page may be closed)\n }\n }\n\n private recordNavigation(url: string, type: NavigationType): void {\n // Finalize network stats for the previous navigation\n if (this.includeNetworkStats && this.currentNetworkCounter && this.records.length > 0) {\n this.records[this.records.length - 1].networkStats = {\n totalRequests: this.currentNetworkCounter.totalRequests,\n failedRequests: this.currentNetworkCounter.failedRequests,\n totalBytes: this.currentNetworkCounter.totalBytes,\n byType: { ...this.currentNetworkCounter.byType },\n };\n }\n\n this.records.push({\n url,\n navigationType: type,\n timestamp: new Date().toISOString(),\n });\n\n // Start fresh counter for this navigation\n if (this.includeNetworkStats) {\n this.currentNetworkCounter = this.createNetworkCounter();\n }\n }\n\n private createNetworkCounter(): NetworkCounter {\n return {\n totalRequests: 0,\n failedRequests: 0,\n totalBytes: 0,\n byType: { xhr: 0, document: 0, script: 0, stylesheet: 0, image: 0, font: 0, other: 0 },\n };\n }\n\n private mapResourceType(type: string): keyof ResourceBreakdown {\n switch (type) {\n case 'xhr':\n case 'fetch':\n return 'xhr';\n case 'document':\n return 'document';\n case 'script':\n return 'script';\n case 'stylesheet':\n return 'stylesheet';\n case 'image':\n return 'image';\n case 'font':\n return 'font';\n default:\n return 'other';\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/fixture.ts","../src/navigation-tracker.ts"],"sourcesContent":["/**\n * @testrelic/playwright-analytics/fixture\n *\n * Extended Playwright test fixture that wraps the default `page` fixture\n * to automatically track navigation events.\n */\n\nimport { test as base, expect } from '@playwright/test';\nimport { NavigationTracker } from './navigation-tracker.js';\n\nexport { expect };\n\nexport const test = base.extend({\n page: async ({ page }, use, testInfo) => {\n const tracker = new NavigationTracker(page as never);\n try {\n await tracker.init();\n } catch {\n // Graceful degradation: continue without SPA detection\n }\n\n await use(page);\n\n try {\n tracker.flush(testInfo);\n } catch {\n // FR-009: fixture without reporter is harmless\n }\n tracker.dispose();\n },\n});\n","/**\n * NavigationTracker — wraps a Playwright Page to track navigation events.\n *\n * Intercepts page methods (goto, goBack, goForward, reload) and listens\n * for DOM events (framenavigated, domcontentloaded, load) plus injected\n * SPA detection scripts.\n */\n\nimport type { NavigationAnnotation, NavigationType, NetworkStats, ResourceBreakdown } from '@testrelic/core';\n\n// Minimal Playwright Page type subset for loose coupling\ninterface PageLike {\n goto(url: string, options?: unknown): Promise<unknown>;\n goBack(options?: unknown): Promise<unknown>;\n goForward(options?: unknown): Promise<unknown>;\n reload(options?: unknown): Promise<unknown>;\n url(): string;\n on(event: string, handler: (...args: unknown[]) => void): void;\n off(event: string, handler: (...args: unknown[]) => void): void;\n addInitScript(script: string | { path?: string } | (() => void)): Promise<void>;\n evaluate(fn: (...args: unknown[]) => unknown, ...args: unknown[]): Promise<unknown>;\n mainFrame(): { url(): string };\n}\n\ninterface TestInfoLike {\n annotations: Array<{ type: string; description?: string }>;\n}\n\ninterface NavigationRecord {\n url: string;\n navigationType: NavigationType;\n timestamp: string;\n domContentLoadedAt?: string;\n networkIdleAt?: string;\n networkStats?: NetworkStats;\n}\n\ninterface NetworkCounter {\n totalRequests: number;\n failedRequests: number;\n failedRequestUrls: string[];\n totalBytes: number;\n byType: Record<keyof ResourceBreakdown, number>;\n}\n\nexport class NavigationTracker {\n private records: NavigationRecord[] = [];\n private listeners: Array<{ event: string; handler: (...args: unknown[]) => void }> = [];\n private origGoto: PageLike['goto'];\n private origGoBack: PageLike['goBack'];\n private origGoForward: PageLike['goForward'];\n private origReload: PageLike['reload'];\n private lastDomContentLoaded: string | undefined;\n private includeNetworkStats: boolean;\n private currentNetworkCounter: NetworkCounter | null = null;\n\n constructor(private page: PageLike, options?: { includeNetworkStats?: boolean }) {\n this.includeNetworkStats = options?.includeNetworkStats ?? true;\n // Store originals\n this.origGoto = page.goto.bind(page);\n this.origGoBack = page.goBack.bind(page);\n this.origGoForward = page.goForward.bind(page);\n this.origReload = page.reload.bind(page);\n\n this.interceptMethods();\n this.attachListeners();\n }\n\n async init(): Promise<void> {\n await this.injectSPADetection();\n }\n\n flush(testInfo: TestInfoLike): void {\n // Finalize network stats for the last navigation\n if (this.includeNetworkStats && this.currentNetworkCounter && this.records.length > 0) {\n this.records[this.records.length - 1].networkStats = {\n totalRequests: this.currentNetworkCounter.totalRequests,\n failedRequests: this.currentNetworkCounter.failedRequests,\n failedRequestUrls: [...this.currentNetworkCounter.failedRequestUrls],\n totalBytes: this.currentNetworkCounter.totalBytes,\n byType: { ...this.currentNetworkCounter.byType },\n };\n }\n\n for (const record of this.records) {\n const annotation: NavigationAnnotation = {\n url: record.url,\n navigationType: record.navigationType,\n timestamp: record.timestamp,\n domContentLoadedAt: record.domContentLoadedAt,\n networkIdleAt: record.networkIdleAt,\n networkStats: record.networkStats,\n };\n testInfo.annotations.push({\n type: 'lambdatest-navigation',\n description: JSON.stringify(annotation),\n });\n }\n }\n\n dispose(): void {\n // Restore original methods\n (this.page as Record<string, unknown>).goto = this.origGoto;\n (this.page as Record<string, unknown>).goBack = this.origGoBack;\n (this.page as Record<string, unknown>).goForward = this.origGoForward;\n (this.page as Record<string, unknown>).reload = this.origReload;\n\n // Remove event listeners\n for (const { event, handler } of this.listeners) {\n this.page.off(event, handler);\n }\n this.listeners = [];\n this.records = [];\n }\n\n getRecords(): readonly NavigationRecord[] {\n return this.records;\n }\n\n private interceptMethods(): void {\n const self = this;\n const page = this.page;\n\n (page as Record<string, unknown>).goto = async function (url: string, options?: unknown) {\n self.recordNavigation(url, 'goto');\n return self.origGoto(url, options);\n };\n\n (page as Record<string, unknown>).goBack = async function (options?: unknown) {\n const result = await self.origGoBack(options);\n self.recordNavigation(page.url(), 'back');\n return result;\n };\n\n (page as Record<string, unknown>).goForward = async function (options?: unknown) {\n const result = await self.origGoForward(options);\n self.recordNavigation(page.url(), 'forward');\n return result;\n };\n\n (page as Record<string, unknown>).reload = async function (options?: unknown) {\n self.recordNavigation(page.url(), 'refresh');\n return self.origReload(options);\n };\n }\n\n private attachListeners(): void {\n // Track DOMContentLoaded for lifecycle timestamps\n const onDomContentLoaded = () => {\n this.lastDomContentLoaded = new Date().toISOString();\n if (this.records.length > 0) {\n this.records[this.records.length - 1].domContentLoadedAt = this.lastDomContentLoaded;\n }\n };\n this.page.on('domcontentloaded', onDomContentLoaded as (...args: unknown[]) => void);\n this.listeners.push({ event: 'domcontentloaded', handler: onDomContentLoaded as (...args: unknown[]) => void });\n\n // Track main frame navigation for non-intercepted navigations (link clicks, form submits)\n const onFrameNavigated = (frame: unknown) => {\n // Only track main frame\n try {\n const frameObj = frame as { url(): string; parentFrame(): unknown };\n if (typeof frameObj.parentFrame === 'function' && frameObj.parentFrame() !== null) {\n return; // Skip sub-frames\n }\n const url = frameObj.url();\n // Skip if we already recorded this URL in the last 50ms (from method interception)\n const lastRecord = this.records[this.records.length - 1];\n if (lastRecord) {\n const timeDiff = Date.now() - new Date(lastRecord.timestamp).getTime();\n if (timeDiff < 50 && lastRecord.url === url) {\n return; // Duplicate from method interception\n }\n }\n // This is an untracked navigation (link click, form submit, etc.)\n this.recordNavigation(url, 'navigation');\n } catch {\n // Ignore frame navigation errors\n }\n };\n this.page.on('framenavigated', onFrameNavigated);\n this.listeners.push({ event: 'framenavigated', handler: onFrameNavigated });\n\n // Track console messages from injected SPA detection script\n const onConsoleMessage = (msg: unknown) => {\n try {\n const msgObj = msg as { text(): string; type(): string };\n if (msgObj.type() !== 'debug') return;\n const text = msgObj.text();\n if (!text.startsWith('__testrelic_nav:')) return;\n const data = JSON.parse(text.slice('__testrelic_nav:'.length));\n if (data.type && data.url) {\n this.recordNavigation(data.url, data.type as NavigationType);\n }\n } catch {\n // Ignore parse errors\n }\n };\n this.page.on('console', onConsoleMessage);\n this.listeners.push({ event: 'console', handler: onConsoleMessage });\n\n // Network stats tracking\n if (this.includeNetworkStats) {\n const onRequest = () => {\n if (this.currentNetworkCounter) {\n this.currentNetworkCounter.totalRequests++;\n }\n };\n this.page.on('request', onRequest);\n this.listeners.push({ event: 'request', handler: onRequest });\n\n const onResponse = (response: unknown) => {\n if (!this.currentNetworkCounter) return;\n try {\n const resp = response as {\n url(): string;\n status(): number;\n headers(): Record<string, string>;\n request(): { resourceType(): string };\n };\n const status = resp.status();\n if (status >= 400) {\n this.currentNetworkCounter.failedRequests++;\n this.currentNetworkCounter.failedRequestUrls.push(status + ' ' + resp.url());\n }\n const contentLength = resp.headers()['content-length'];\n if (contentLength) {\n this.currentNetworkCounter.totalBytes += parseInt(contentLength, 10) || 0;\n }\n const resourceType = resp.request().resourceType();\n const typeKey = this.mapResourceType(resourceType);\n this.currentNetworkCounter.byType[typeKey]++;\n } catch {\n // Ignore response processing errors\n }\n };\n this.page.on('response', onResponse);\n this.listeners.push({ event: 'response', handler: onResponse });\n\n const onRequestFailed = (request: unknown) => {\n if (this.currentNetworkCounter) {\n this.currentNetworkCounter.failedRequests++;\n try {\n const req = request as { url(): string };\n this.currentNetworkCounter.failedRequestUrls.push('ERR ' + req.url());\n } catch {\n // Ignore\n }\n }\n };\n this.page.on('requestfailed', onRequestFailed);\n this.listeners.push({ event: 'requestfailed', handler: onRequestFailed });\n }\n }\n\n private async injectSPADetection(): Promise<void> {\n try {\n await this.page.addInitScript(() => {\n const origPush = history.pushState.bind(history);\n const origReplace = history.replaceState.bind(history);\n\n history.pushState = function (...args: Parameters<typeof origPush>) {\n origPush(...args);\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'spa_route',\n url: location.href,\n }));\n };\n\n history.replaceState = function (...args: Parameters<typeof origReplace>) {\n origReplace(...args);\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'spa_replace',\n url: location.href,\n }));\n };\n\n window.addEventListener('popstate', () => {\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'popstate',\n url: location.href,\n }));\n });\n\n window.addEventListener('hashchange', () => {\n // eslint-disable-next-line no-console\n console.debug('__testrelic_nav:' + JSON.stringify({\n type: 'hash_change',\n url: location.href,\n }));\n });\n });\n } catch {\n // Ignore injection errors (page may be closed)\n }\n }\n\n private recordNavigation(url: string, type: NavigationType): void {\n // Finalize network stats for the previous navigation\n if (this.includeNetworkStats && this.currentNetworkCounter && this.records.length > 0) {\n this.records[this.records.length - 1].networkStats = {\n totalRequests: this.currentNetworkCounter.totalRequests,\n failedRequests: this.currentNetworkCounter.failedRequests,\n failedRequestUrls: [...this.currentNetworkCounter.failedRequestUrls],\n totalBytes: this.currentNetworkCounter.totalBytes,\n byType: { ...this.currentNetworkCounter.byType },\n };\n }\n\n this.records.push({\n url,\n navigationType: type,\n timestamp: new Date().toISOString(),\n });\n\n // Start fresh counter for this navigation\n if (this.includeNetworkStats) {\n this.currentNetworkCounter = this.createNetworkCounter();\n }\n }\n\n private createNetworkCounter(): NetworkCounter {\n return {\n totalRequests: 0,\n failedRequests: 0,\n failedRequestUrls: [],\n totalBytes: 0,\n byType: { xhr: 0, document: 0, script: 0, stylesheet: 0, image: 0, font: 0, other: 0 },\n };\n }\n\n private mapResourceType(type: string): keyof ResourceBreakdown {\n switch (type) {\n case 'xhr':\n case 'fetch':\n return 'xhr';\n case 'document':\n return 'document';\n case 'script':\n return 'script';\n case 'stylesheet':\n return 'stylesheet';\n case 'image':\n return 'image';\n case 'font':\n return 'font';\n default:\n return 'other';\n }\n }\n}\n"],"mappings":"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","names":[]}