diff --git a/package.json b/package.json index e962049..2fb3318 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 0174d7e..897dbeb 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 4e43790..a65b992 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 0000000..ed26844 --- /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 4f8021b..b1ef93d 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 +}