Skip to content

Upgrade to WebAssembly version of source-map #98

@zeorin

Description

@zeorin

source-map v0.7.x uses WebAssembly for a remarkably faster parsing of source maps. StackTraceGPS is compatible with it with only very minor changes.

Expected Behavior

StackTraceGPS uses WebAssembly to parse source maps.

Context

Parsing modern web app source maps on the client in JavaScript leads to an unusable user experience, locking up the browser for long enough that the user may suspect the app has crashed, and thus force-quits the browser before we could send the parsed error to our server.

Possible Solution

I'm currently enabling it as follows:

package.json:

{
  // …
  "resolutions": {
    "stacktrace-gps@npm:3.1.2/source-map@npm:0.5.6": "npm:source-map@^0.7.6"
  }
}

./stacktrace-gps.ts:

import type * as StackFrame from "stackframe"
import type * as StackTrace from "stacktrace-js"
import { SourceMapConsumer } from "source-map"
import mappingsUrl from "source-map/lib/mappings.wasm?url"
import StackTraceGPS from "stacktrace-gps"

SourceMapConsumer.initialize({ "lib/mappings.wasm": mappingsUrl })

interface _StackTraceGPS extends StackTraceGPS {
  sourceMapConsumerCache: {
    [sourceMappingUrl: string]: SourceMapConsumer | Promise<SourceMapConsumer>
  }

  pinpoint(stackframe: StackFrame | StackTrace.StackFrame): Promise<StackFrame>

  findFunctionName(
    stackframe: StackFrame | StackTrace.StackFrame,
  ): Promise<StackFrame>

  getMappedLocation(
    stackframe: StackFrame | StackTrace.StackFrame,
  ): Promise<StackFrame>

  [Symbol.dispose](): void
}

// polyfill for `Symbol.dispose`
Symbol.dispose ??= Symbol("Symbol.dispose")

class _StackTraceGPS extends StackTraceGPS {
  [Symbol.dispose]() {
    for (const sourceMapConsumerPromise of Object.values(
      this.sourceMapConsumerCache,
    )) {
      Promise.resolve(sourceMapConsumerPromise).then((sourceMapConsumer) => {
        return sourceMapConsumer.destroy()
      })
    }
  }
}

export { _StackTraceGPS as StackTraceGPS }

./report-error.ts:

import { StackTraceGPS } from "./stacktrace-gps.ts"

const report = async (error: Error) => {
  using gps = new StackTraceGPS()
  const stack = await Promise.all(
    (await StackTrace.fromError(payload.error, { offline: true })).map(
      async (stackframe) => {
        try {
          return await gps.pinpoint(stackframe)
        } catch {
          return stackframe
        }
      },
    ),
  )

  // report the error…
}

For users that can't use using, a helper with function, modelled on source-map's SourceMapConsumer.with, or a destroy method could work well.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions