From 33d1b71e9af78b25be1f5b2b023a76b7d6dbc149 Mon Sep 17 00:00:00 2001 From: Roshan Gorasia Date: Mon, 19 Oct 2026 13:59:21 +0100 Subject: [PATCH] fix: only report window errors thrown by processout.js The global error handler reported any error whose file was on a ProcessOut host. Some browsers attribute errors from their injected scripts to the page URL, so errors that aren't ours were reported. Restrict reporting to the processout.js script and skip events with no error object. Co-Authored-By: Claude Opus 5.5 --- package.json | 2 +- src/processout/processout.ts | 2 +- src/processout/telemetry.ts | 12 +++++++++ test/processout/telemetry.test.ts | 41 +++++++++++++++++++++++++++++++ test/support/loadNamespace.ts | 11 +++++++++ 5 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 test/processout/telemetry.test.ts diff --git a/package.json b/package.json index e9620490..2fb33180 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "processout.js", - "version": "1.9.15", + "version": "1.9.16", "description": "ProcessOut.js is a JavaScript library for ProcessOut's payment processing API.", "scripts": { "build:processout": "tsc -p src/processout && uglifyjs --compress --keep-fnames --ie8 dist/processout.js -o dist/processout.js", diff --git a/src/processout/processout.ts b/src/processout/processout.ts index 0174d7e9..897dbeb7 100644 --- a/src/processout/processout.ts +++ b/src/processout/processout.ts @@ -239,7 +239,7 @@ module ProcessOut { return } - if (/^https?:\/\/.*\.processout\.((com)|(ninja)|(dev))\//.test(event.filename)) { + if (isProcessOutScriptError(event)) { setTimeout(() => { this.telemetryClient.reportError({ host: window && window.location ? window.location.host : "", diff --git a/src/processout/telemetry.ts b/src/processout/telemetry.ts index 4e437906..a65b9923 100644 --- a/src/processout/telemetry.ts +++ b/src/processout/telemetry.ts @@ -24,6 +24,18 @@ module ProcessOut { type LogLevel = "error" | "warn" | "info" | "debug" + const processOutScriptURL = + /^https?:\/\/[^/]*\.processout\.(com|ninja|dev)\/processout\.js([?#]|$)/ + + /** + * Whether a window error event was thrown by processout.js itself. Some + * browsers attribute errors from their injected scripts to the page URL, so + * matching any ProcessOut-hosted file reports errors that aren't ours. + */ + export function isProcessOutScriptError(event: ErrorEvent): boolean { + return !!event && !!event.error && processOutScriptURL.test(event.filename || "") + } + export class TelemetryClient { protected processOutInstance: ProcessOut diff --git a/test/processout/telemetry.test.ts b/test/processout/telemetry.test.ts new file mode 100644 index 00000000..ed26844a --- /dev/null +++ b/test/processout/telemetry.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest" +import { loadTelemetry } from "../support/loadNamespace" + +const { isProcessOutScriptError } = loadTelemetry() + +function errorEvent(filename: string, error: unknown = new Error("boom")) { + return { filename, error } as unknown as ErrorEvent +} + +describe("isProcessOutScriptError", () => { + it("accepts errors thrown by processout.js", () => { + expect(isProcessOutScriptError(errorEvent("https://js.processout.com/processout.js"))).toBe( + true, + ) + expect( + isProcessOutScriptError(errorEvent("https://js.processout.ninja/processout.js?v=1")), + ).toBe(true) + }) + + it("rejects errors attributed to a processout-hosted page", () => { + const page = "https://checkout.processout.com/some/page" + expect(isProcessOutScriptError(errorEvent(page))).toBe(false) + }) + + it("rejects other scripts on processout or lookalike hosts", () => { + expect(isProcessOutScriptError(errorEvent("https://js.processout.com/modal.js"))).toBe(false) + expect(isProcessOutScriptError(errorEvent("https://js.processout.com/processout.json"))).toBe( + false, + ) + expect( + isProcessOutScriptError(errorEvent("https://processout.com.evil.io/processout.js")), + ).toBe(false) + expect(isProcessOutScriptError(errorEvent("https://merchant.example/app.js"))).toBe(false) + }) + + it("rejects events without an error object", () => { + expect( + isProcessOutScriptError(errorEvent("https://js.processout.com/processout.js", null)), + ).toBe(false) + }) +}) diff --git a/test/support/loadNamespace.ts b/test/support/loadNamespace.ts index 4f8021ba..b1ef93dd 100644 --- a/test/support/loadNamespace.ts +++ b/test/support/loadNamespace.ts @@ -156,3 +156,14 @@ export function loadDynamicCheckout(): DynamicCheckoutNamespace { return { namespace: moduleShim.exports, dispatchedEvents } } + +/** + * Load `src/processout/telemetry.ts` and return its `ProcessOut` namespace. + */ +export function loadTelemetry(): Record { + const js = compile("src/processout/telemetry.ts") + const moduleShim = { exports: {} as Record } + const run = new Function("module", "exports", `${js}\nmodule.exports = ProcessOut;`) + run(moduleShim, moduleShim.exports) + return moduleShim.exports +}