diff --git a/package-lock.json b/package-lock.json index e16b4edf..cb9928e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12066,10 +12066,10 @@ }, "packages/angular": { "name": "@appsignal/angular", - "version": "1.0.16", + "version": "1.0.17", "license": "MIT", "dependencies": { - "@appsignal/javascript": "=1.5.0" + "@appsignal/javascript": "=1.6.0" }, "peerDependencies": { "@angular/core": ">= 8.0.0" @@ -12091,10 +12091,10 @@ }, "packages/ember": { "name": "@appsignal/ember", - "version": "1.0.16", + "version": "1.0.17", "license": "MIT", "dependencies": { - "@appsignal/javascript": "=1.5.0" + "@appsignal/javascript": "=1.6.0" }, "peerDependencies": { "ember-source": ">= 3.11.1" @@ -12102,7 +12102,7 @@ }, "packages/javascript": { "name": "@appsignal/javascript", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -12110,42 +12110,42 @@ }, "packages/plugin-breadcrumbs-console": { "name": "@appsignal/plugin-breadcrumbs-console", - "version": "1.1.35", + "version": "1.1.36", "license": "MIT", "dependencies": { - "@appsignal/javascript": "=1.5.0" + "@appsignal/javascript": "=1.6.0" } }, "packages/plugin-breadcrumbs-network": { "name": "@appsignal/plugin-breadcrumbs-network", - "version": "1.1.22", + "version": "1.1.23", "license": "MIT", "dependencies": { - "@appsignal/javascript": "=1.5.0" + "@appsignal/javascript": "=1.6.0" } }, "packages/plugin-path-decorator": { "name": "@appsignal/plugin-path-decorator", - "version": "1.0.16", + "version": "1.0.17", "license": "MIT", "dependencies": { - "@appsignal/javascript": "=1.5.0" + "@appsignal/javascript": "=1.6.0" } }, "packages/plugin-window-events": { "name": "@appsignal/plugin-window-events", - "version": "1.0.24", + "version": "1.0.25", "license": "MIT", "dependencies": { - "@appsignal/javascript": "=1.5.0" + "@appsignal/javascript": "=1.6.0" } }, "packages/preact": { "name": "@appsignal/preact", - "version": "1.0.25", + "version": "1.0.26", "license": "MIT", "dependencies": { - "@appsignal/javascript": "=1.5.0" + "@appsignal/javascript": "=1.6.0" }, "peerDependencies": { "preact": "^10.0.0" @@ -12153,10 +12153,10 @@ }, "packages/react": { "name": "@appsignal/react", - "version": "1.0.29", + "version": "1.0.30", "license": "MIT", "dependencies": { - "@appsignal/javascript": "=1.5.0" + "@appsignal/javascript": "=1.6.0" }, "peerDependencies": { "react": ">= 16.8.6 < 20" @@ -12164,10 +12164,10 @@ }, "packages/stimulus": { "name": "@appsignal/stimulus", - "version": "1.0.19", + "version": "1.0.20", "license": "MIT", "dependencies": { - "@appsignal/javascript": "=1.5.0" + "@appsignal/javascript": "=1.6.0" }, "peerDependencies": { "@hotwired/stimulus": "^3.0", @@ -12184,10 +12184,10 @@ }, "packages/vue": { "name": "@appsignal/vue", - "version": "1.1.5", + "version": "1.1.6", "license": "MIT", "dependencies": { - "@appsignal/javascript": "=1.5.0" + "@appsignal/javascript": "=1.6.0" }, "peerDependencies": { "vue": ">= 2.6.0" @@ -12195,7 +12195,7 @@ }, "packages/webpack": { "name": "@appsignal/webpack", - "version": "1.1.6", + "version": "1.1.7", "license": "MIT", "dependencies": { "axios": "^1.8.2", diff --git a/packages/javascript/.changesets/allow-functions-as-backtrace-matchers.md b/packages/javascript/.changesets/allow-functions-as-backtrace-matchers.md new file mode 100644 index 00000000..bbe16f76 --- /dev/null +++ b/packages/javascript/.changesets/allow-functions-as-backtrace-matchers.md @@ -0,0 +1,19 @@ +--- +bump: patch +type: add +--- + +Allow functions as backtrace matchers. Alongside regular expressions, you can also provide custom functions to match and replace paths in the backtrace: + +```javascript +const appsignal = new Appsignal({ + // ... + matchBacktracePaths: [(path) => { + if (path.indexOf("/bundle/") !== -1) { + return "bundle.js" + } + }] +}) +``` + +The function must take a backtrace line path as an argument. When the function returns a non-empty string, the string will be used as the path for that backtrace line. Otherwise, the path will be left unchanged. diff --git a/packages/javascript/src/__tests__/index.test.ts b/packages/javascript/src/__tests__/index.test.ts index b5ca6e64..43be734b 100644 --- a/packages/javascript/src/__tests__/index.test.ts +++ b/packages/javascript/src/__tests__/index.test.ts @@ -97,13 +97,17 @@ describe("Appsignal", () => { appsignal = new Appsignal({ key: "TESTKEY", namespace: "test", - matchBacktracePaths: [/here(.*)/g] + matchBacktracePaths: [ + /here(.*)/g, + path => (path.indexOf("some") !== -1 ? "some.js" : undefined) + ] }) const error = new Error("test error") error.stack = [ "Error: test error", - " at Foo (http://localhost:8080/here/istheapp.js:13:10)" + " at Foo (http://localhost:8080/here/istheapp.js:13:10)", + " at Bar (http://localhost:8080/some/thingelse.js:13:10)" ].join("\n") appsignal.send(error) @@ -112,10 +116,11 @@ describe("Appsignal", () => { expect(firstPayload.error.backtrace).toEqual([ "Error: test error", - " at Foo (/istheapp.js:13:10)" + " at Foo (/istheapp.js:13:10)", + " at Bar (some.js:13:10)" ]) - expect(firstPayload.environment.backtrace_paths_matched).toEqual("1") + expect(firstPayload.environment.backtrace_paths_matched).toEqual("2") // As the regex used has the `g` flag, it would // remember the last matched position and fail to match again: @@ -124,7 +129,7 @@ describe("Appsignal", () => { appsignal.send(error) const secondPayload = pushMockCall(1) - expect(secondPayload.environment.backtrace_paths_matched).toEqual("1") + expect(secondPayload.environment.backtrace_paths_matched).toEqual("2") }) }) diff --git a/packages/javascript/src/__tests__/span.test.ts b/packages/javascript/src/__tests__/span.test.ts index 7c61295a..6ce60a45 100644 --- a/packages/javascript/src/__tests__/span.test.ts +++ b/packages/javascript/src/__tests__/span.test.ts @@ -1,4 +1,4 @@ -import { Span } from "../span" +import { Span, toBacktraceMatcher } from "../span" describe("Span", () => { let span: Span @@ -71,7 +71,9 @@ describe("Span", () => { ].join("\n") span.setError(error) - span.cleanBacktracePath([new RegExp("/assets/(app/.*)$")]) + span.cleanBacktracePath( + [new RegExp("/assets/(app/.*)$")].map(toBacktraceMatcher) + ) const backtrace = span.serialize().error.backtrace expect(backtrace).toEqual([ @@ -97,7 +99,9 @@ describe("Span", () => { ].join("\n") span.setError(error) - span.cleanBacktracePath([new RegExp("/assets/(app/.*)$")]) + span.cleanBacktracePath( + [new RegExp("/assets/(app/.*)$")].map(toBacktraceMatcher) + ) const backtrace = span.serialize().error.backtrace expect(backtrace).toEqual([ @@ -123,9 +127,11 @@ describe("Span", () => { ].join("\n") span.setError(error) - span.cleanBacktracePath([ - new RegExp(".*/(assets/)(?:[0-9a-f]{16}/)?(app/.*)$") - ]) + span.cleanBacktracePath( + [new RegExp(".*/(assets/)(?:[0-9a-f]{16}/)?(app/.*)$")].map( + toBacktraceMatcher + ) + ) const backtrace = span.serialize().error.backtrace expect(backtrace).toEqual([ @@ -149,14 +155,16 @@ describe("Span", () => { ].join("\n") span.setError(error) - span.cleanBacktracePath([ - // This can only match `Bar`. - new RegExp("/assets/[0-9a-f]{16}/(.*)$"), - - // This can match both `Foo` and `Bar`, but should only - // match `Foo` because the previous matcher takes precedence. - new RegExp("/assets/(.*)$") - ]) + span.cleanBacktracePath( + [ + // This can only match `Bar`. + new RegExp("/assets/[0-9a-f]{16}/(.*)$"), + + // This can match both `Foo` and `Bar`, but should only + // match `Foo` because the previous matcher takes precedence. + new RegExp("/assets/(.*)$") + ].map(toBacktraceMatcher) + ) const backtrace = span.serialize().error.backtrace expect(backtrace).toEqual([ @@ -181,7 +189,7 @@ describe("Span", () => { // empty string. // // This should result in the line not being modified. - span.cleanBacktracePath([new RegExp(".*")]) + span.cleanBacktracePath([new RegExp(".*")].map(toBacktraceMatcher)) const backtrace = span.serialize().error.backtrace expect(backtrace).toEqual([ @@ -203,7 +211,7 @@ describe("Span", () => { // empty string. // // This should result in the line not being modified. - span.cleanBacktracePath([new RegExp(".*(z*)$")]) + span.cleanBacktracePath([new RegExp(".*(z*)$")].map(toBacktraceMatcher)) const backtrace = span.serialize().error.backtrace expect(backtrace).toEqual([ @@ -220,7 +228,9 @@ describe("Span", () => { ].join("\n") span.setError(error) - span.cleanBacktracePath([new RegExp("^pancakes/(.*)$")]) + span.cleanBacktracePath( + [new RegExp("^pancakes/(.*)$")].map(toBacktraceMatcher) + ) const backtrace = span.serialize().error.backtrace expect(backtrace).toEqual([ @@ -229,6 +239,32 @@ describe("Span", () => { expect(span.serialize().environment).toBeUndefined() }) + + it("can be used with custom backtrace matcher functions", () => { + const error = new Error("test error") + error.stack = [ + "Foo@http://localhost:8080/assets/app/first.js:13:10", + "Bar@http://localhost:8080/assets/app/second.js:13:10", + "Baz@http://localhost:8080/assets/app/third.js:13:10" + ].join("\n") + + span.setError(error) + span.cleanBacktracePath([ + path => (path.indexOf("first") !== -1 ? "foo.js" : undefined), + path => (path.indexOf("second") !== -1 ? "bar.js" : undefined) + ]) + + const backtrace = span.serialize().error.backtrace + expect(backtrace).toEqual([ + "Foo@foo.js:13:10", + "Bar@bar.js:13:10", + "Baz@http://localhost:8080/assets/app/third.js:13:10" + ]) + + expect(span.serialize().environment).toMatchObject({ + backtrace_paths_matched: "2" + }) + }) }) describe("getAction", () => { diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index f2d02acb..dfef6a5a 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -14,17 +14,17 @@ import { toHashMap } from "./hashmap" import { VERSION } from "./version" import { PushApi } from "./api" import { Environment } from "./environment" -import { Span } from "./span" +import { Span, toBacktraceMatcher } from "./span" export { Span } import { Queue } from "./queue" import { Dispatcher } from "./dispatcher" -import { AppsignalOptions } from "./options" +import { AppsignalOptions, BacktraceMatcher } from "./options" export default class Appsignal { public VERSION = VERSION public ignored: RegExp[] = [] - private matchBacktracePaths: RegExp[] = [] + private backtraceMatchers: BacktraceMatcher[] = [] private _dispatcher: Dispatcher private _options: AppsignalOptions @@ -83,15 +83,21 @@ export default class Appsignal { } if (matchBacktracePaths) { + let paths: (RegExp | BacktraceMatcher)[] + if (Array.isArray(matchBacktracePaths)) { - this.matchBacktracePaths = matchBacktracePaths + paths = matchBacktracePaths } else { - this.matchBacktracePaths = [matchBacktracePaths] + paths = [matchBacktracePaths] } - this.matchBacktracePaths = this.matchBacktracePaths - .filter(value => value instanceof RegExp) - .map(unglobalize) + for (const matcher of paths) { + if (matcher instanceof RegExp) { + this.backtraceMatchers.push(toBacktraceMatcher(unglobalize(matcher))) + } else if (typeof matcher === "function") { + this.backtraceMatchers.push(matcher) + } + } } this._dispatcher = new Dispatcher(this._queue, this._api) @@ -242,7 +248,7 @@ export default class Appsignal { return } - span.cleanBacktracePath(this.matchBacktracePaths) + span.cleanBacktracePath(this.backtraceMatchers) if (Environment.supportsPromises()) { // clear breadcrumbs as they are now loaded into the span, diff --git a/packages/javascript/src/options.ts b/packages/javascript/src/options.ts index 06e8da44..36bdd976 100644 --- a/packages/javascript/src/options.ts +++ b/packages/javascript/src/options.ts @@ -3,11 +3,16 @@ interface BaseOptions { uri?: string } +export type BacktraceMatcher = (path: string) => string | undefined + export interface AppsignalOptions extends BaseOptions { namespace?: string revision?: string ignoreErrors?: RegExp[] - matchBacktracePaths?: RegExp | RegExp[] + matchBacktracePaths?: + | RegExp + | BacktraceMatcher + | (RegExp | BacktraceMatcher)[] } export interface PushApiOptions extends BaseOptions { diff --git a/packages/javascript/src/span.ts b/packages/javascript/src/span.ts index 24798258..a006dfb3 100644 --- a/packages/javascript/src/span.ts +++ b/packages/javascript/src/span.ts @@ -5,6 +5,7 @@ import type { HashMap, HashMapValue } from "./hashmap" import type { Breadcrumb } from "./breadcrumb" import type { SpanError } from "./error" +import { BacktraceMatcher } from "./options" /** * The internal data structure of a `Span` inside the JavaScript integration. @@ -116,7 +117,7 @@ export class Span extends Serializable { // @private // Do not use this function directly. Instead, set the `matchBacktracePaths` // configuration option when initializing AppSignal. - public cleanBacktracePath(matchBacktracePaths: RegExp[]): this { + public cleanBacktracePath(matchBacktracePaths: BacktraceMatcher[]): this { if (matchBacktracePaths.length === 0) { return this } @@ -133,17 +134,14 @@ export class Span extends Serializable { return line } - for (const matcher of matchBacktracePaths as RegExp[]) { - const match = path.match(matcher) - if (!match || match.length < 2) { + for (const matcher of matchBacktracePaths) { + const relevantPath = matcher(path) + if (!relevantPath) { continue } - const relevantPath = match.slice(1).join("") - if (relevantPath) { - linesMatched++ - return line.replace(path, relevantPath) - } + linesMatched++ + return line.replace(path, relevantPath) } return line @@ -159,6 +157,23 @@ export class Span extends Serializable { } } +// @private +// Do not use this function directly. Instead, set the `matchBacktracePaths` +// configuration option when initializing AppSignal. +// +// Converts a `RegExp` object into a `BacktraceMatcher` function that returns +// the concatenated matches from that object. +export function toBacktraceMatcher(regexp: RegExp): BacktraceMatcher { + return (path: string): string | undefined => { + const match = path.match(regexp) + if (!match || match.length < 2) { + return + } + + return match.slice(1).join("") + } +} + // Backtrace formats are different between browsers, and generally not // meant to be parsed by machines. This function does a best-effort to // extract whatever is at the location in the line where the path