Repository: DataDog/browser-sdk
Pull request #3481: 🐛 [RUM-9465] Add support for parsing Safari Wasm stack trace
Author: login: rgaignault
Created: 2025-04-14T13:57:01Z | Merged: 2025-04-22T14:15:43Z

## Motivation

Add support for parsing correctly stack trace in a safari wasm context. Cf https://github.com/DataDog/browser-sdk/issues/3479

## Changes

Modify computeStackTrace function / add unit test

## Testing

<!-- How can the reviewer confirm these changes do what you say they do? Are there automated tests? -->

- [ ] Local
- [ ] Staging
- [ ] Unit
- [ ] End to end

---

I have gone over the [contributing](https://github.com/DataDog/browser-sdk/blob/main/CONTRIBUTING.md) documentation.



Diff:
diff --git a/packages/core/src/tools/stackTrace/computeStackTrace.spec.ts b/packages/core/src/tools/stackTrace/computeStackTrace.spec.ts
index 191db01f42..f12ea4f904 100644
--- a/packages/core/src/tools/stackTrace/computeStackTrace.spec.ts
+++ b/packages/core/src/tools/stackTrace/computeStackTrace.spec.ts
@@ -90,11 +90,7 @@ Error: foo
   })
 
   it('should handle a native error object stack from Chrome', () => {
-    const mockErr = {
-      message: 'foo',
-      name: 'Error',
-      stack: stackStr,
-    }
+    const mockErr = { message: 'foo', name: 'Error', stack: stackStr }
     const stackFrames = computeStackTrace(mockErr)
 
     expect(stackFrames.stack[0].url).toEqual('<anonymous>')
@@ -173,20 +169,8 @@ Error: foo
     const stackFrames = computeStackTrace(CapturedExceptions.SAFARI_7)
 
     expect(stackFrames.stack.length).toEqual(3)
-    expect(stackFrames.stack[0]).toEqual({
-      args: [],
-      column: 22,
-      func: '?',
-      line: 48,
-      url: 'http://path/to/file.js',
-    })
-    expect(stackFrames.stack[1]).toEqual({
-      args: [],
-      column: 15,
-      func: 'foo',
-      line: 52,
-      url: 'http://path/to/file.js',
-    })
+    expect(stackFrames.stack[0]).toEqual({ args: [], column: 22, func: '?', line: 48, url: 'http://path/to/file.js' })
+    expect(stackFrames.stack[1]).toEqual({ args: [], column: 15, func: 'foo', line: 52, url: 'http://path/to/file.js' })
     expect(stackFrames.stack[2]).toEqual({
       args: [],
       column: 107,
@@ -200,20 +184,8 @@ Error: foo
     const stackFrames = computeStackTrace(CapturedExceptions.SAFARI_8)
 
     expect(stackFrames.stack.length).toEqual(3)
-    expect(stackFrames.stack[0]).toEqual({
-      args: [],
-      column: 22,
-      func: '?',
-      line: 47,
-      url: 'http://path/to/file.js',
-    })
-    expect(stackFrames.stack[1]).toEqual({
-      args: [],
-      column: 15,
-      func: 'foo',
-      line: 52,
-      url: 'http://path/to/file.js',
-    })
+    expect(stackFrames.stack[0]).toEqual({ args: [], column: 22, func: '?', line: 47, url: 'http://path/to/file.js' })
+    expect(stackFrames.stack[1]).toEqual({ args: [], column: 15, func: 'foo', line: 52, url: 'http://path/to/file.js' })
     expect(stackFrames.stack[2]).toEqual({
       args: [],
       column: 23,
@@ -236,13 +208,7 @@ Error: foo
       line: undefined,
       url: '[native code]',
     })
-    expect(stackFrames.stack[1]).toEqual({
-      args: [],
-      column: 21,
-      func: 'foo',
-      line: 58,
-      url: 'http://path/to/file.js',
-    })
+    expect(stackFrames.stack[1]).toEqual({ args: [], column: 21, func: 'foo', line: 58, url: 'http://path/to/file.js' })
     expect(stackFrames.stack[2]).toEqual({
       args: [],
       column: 91,
@@ -393,20 +359,8 @@ Error: foo
     const stackFrames = computeStackTrace(CapturedExceptions.FIREFOX_31)
 
     expect(stackFrames.stack.length).toEqual(3)
-    expect(stackFrames.stack[0]).toEqual({
-      args: [],
-      column: 13,
-      func: 'foo',
-      line: 41,
-      url: 'http://path/to/file.js',
-    })
-    expect(stackFrames.stack[1]).toEqual({
-      args: [],
-      column: 1,
-      func: 'bar',
-      line: 1,
-      url: 'http://path/to/file.js',
-    })
+    expect(stackFrames.stack[0]).toEqual({ args: [], column: 13, func: 'foo', line: 41, url: 'http://path/to/file.js' })
+    expect(stackFrames.stack[1]).toEqual({ args: [], column: 1, func: 'bar', line: 1, url: 'http://path/to/file.js' })
     expect(stackFrames.stack[2]).toEqual({
       args: [],
       column: 1,
@@ -434,13 +388,7 @@ Error: foo
       line: 15,
       url: 'file:///path/to/file.js',
     })
-    expect(stackFrames.stack[2]).toEqual({
-      args: [],
-      column: 3,
-      func: 'bar',
-      line: 20,
-      url: 'file:///path/to/file.js',
-    })
+    expect(stackFrames.stack[2]).toEqual({ args: [], column: 3, func: 'bar', line: 20, url: 'file:///path/to/file.js' })
     expect(stackFrames.stack[3]).toEqual({
       args: [],
       column: 1,
@@ -485,13 +433,7 @@ Error: foo
       line: 128,
       url: 'std_interceptors.js',
     })
-    expect(stackFrames.stack[2]).toEqual({
-      args: [],
-      column: 29,
-      func: 'eval',
-      line: 132,
-      url: 'std_interceptors.jsx',
-    })
+    expect(stackFrames.stack[2]).toEqual({ args: [], column: 29, func: 'eval', line: 132, url: 'std_interceptors.jsx' })
   })
 
   it('should not include error message into stacktrace ', () => {
@@ -527,34 +469,10 @@ Error: foo
     const stackFrames = computeStackTrace(CapturedExceptions.CHROME_15 as any)
 
     expect(stackFrames.stack.length).toEqual(4)
-    expect(stackFrames.stack[0]).toEqual({
-      args: [],
-      column: 17,
-      func: 'bar',
-      line: 13,
-      url: 'http://path/to/file.js',
-    })
-    expect(stackFrames.stack[1]).toEqual({
-      args: [],
-      column: 5,
-      func: 'bar',
-      line: 16,
-      url: 'http://path/to/file.js',
-    })
-    expect(stackFrames.stack[2]).toEqual({
-      args: [],
-      column: 5,
-      func: 'foo',
-      line: 20,
-      url: 'http://path/to/file.js',
-    })
-    expect(stackFrames.stack[3]).toEqual({
-      args: [],
-      column: 4,
-      func: '?',
-      line: 24,
-      url: 'http://path/to/file.js',
-    })
+    expect(stackFrames.stack[0]).toEqual({ args: [], column: 17, func: 'bar', line: 13, url: 'http://path/to/file.js' })
+    expect(stackFrames.stack[1]).toEqual({ args: [], column: 5, func: 'bar', line: 16, url: 'http://path/to/file.js' })
+    expect(stackFrames.stack[2]).toEqual({ args: [], column: 5, func: 'foo', line: 20, url: 'http://path/to/file.js' })
+    expect(stackFrames.stack[3]).toEqual({ args: [], column: 4, func: '?', line: 24, url: 'http://path/to/file.js' })
   })
 
   it('should parse Chrome 36 error with port numbers', () => {
@@ -738,13 +656,7 @@ Error: foo
     const stackFrames = computeStackTrace(CapturedExceptions.CHROME_111_SNIPPET)
 
     expect(stackFrames.stack.length).toEqual(1)
-    expect(stackFrames.stack[0]).toEqual({
-      args: [],
-      column: 13,
-      func: '?',
-      line: 1,
-      url: 'snippet:///snippet_file',
-    })
+    expect(stackFrames.stack[0]).toEqual({ args: [], column: 13, func: '?', line: 1, url: 'snippet:///snippet_file' })
   })
 
   it('should parse empty IE 9 error', () => {
@@ -767,20 +679,8 @@ Error: foo
       line: 48,
       url: 'http://path/to/file.js',
     })
-    expect(stackFrames.stack[1]).toEqual({
-      args: [],
-      column: 9,
-      func: 'foo',
-      line: 46,
-      url: 'http://path/to/file.js',
-    })
-    expect(stackFrames.stack[2]).toEqual({
-      args: [],
-      column: 1,
-      func: 'bar',
-      line: 82,
-      url: 'http://path/to/file.js',
-    })
+    expect(stackFrames.stack[1]).toEqual({ args: [], column: 9, func: 'foo', line: 46, url: 'http://path/to/file.js' })
+    expect(stackFrames.stack[2]).toEqual({ args: [], column: 1, func: 'bar', line: 82, url: 'http://path/to/file.js' })
   })
 
   it('should parse IE 11 error', () => {
@@ -795,67 +695,25 @@ Error: foo
       line: 47,
       url: 'http://path/to/file.js',
     })
-    expect(stackFrames.stack[1]).toEqual({
-      args: [],
-      column: 13,
-      func: 'foo',
-      line: 45,
-      url: 'http://path/to/file.js',
-    })
-    expect(stackFrames.stack[2]).toEqual({
-      args: [],
-      column: 1,
-      func: 'bar',
-      line: 108,
-      url: 'http://path/to/file.js',
-    })
+    expect(stackFrames.stack[1]).toEqual({ args: [], column: 13, func: 'foo', line: 45, url: 'http://path/to/file.js' })
+    expect(stackFrames.stack[2]).toEqual({ args: [], column: 1, func: 'bar', line: 108, url: 'http://path/to/file.js' })
   })
 
   it('should parse IE 11 eval error', () => {
     const stackFrames = computeStackTrace(CapturedExceptions.IE_11_EVAL)
 
     expect(stackFrames.stack.length).toEqual(3)
-    expect(stackFrames.stack[0]).toEqual({
-      args: [],
-      column: 1,
-      func: 'eval code',
-      line: 1,
-      url: 'eval code',
-    })
-    expect(stackFrames.stack[1]).toEqual({
-      args: [],
-      column: 17,
-      func: 'foo',
-      line: 58,
-      url: 'http://path/to/file.js',
-    })
-    expect(stackFrames.stack[2]).toEqual({
-      args: [],
-      column: 1,
-      func: 'bar',
-      line: 109,
-      url: 'http://path/to/file.js',
-    })
+    expect(stackFrames.stack[0]).toEqual({ args: [], column: 1, func: 'eval code', line: 1, url: 'eval code' })
+    expect(stackFrames.stack[1]).toEqual({ args: [], column: 17, func: 'foo', line: 58, url: 'http://path/to/file.js' })
+    expect(stackFrames.stack[2]).toEqual({ args: [], column: 1, func: 'bar', line: 109, url: 'http://path/to/file.js' })
   })
 
   it('should parse Opera 25 error', () => {
     const stackFrames = computeStackTrace(CapturedExceptions.OPERA_25)
 
     expect(stackFrames.stack.length).toEqual(3)
-    expect(stackFrames.stack[0]).toEqual({
-      args: [],
-      column: 22,
-      func: '?',
-      line: 47,
-      url: 'http://path/to/file.js',
-    })
-    expect(stackFrames.stack[1]).toEqual({
-      args: [],
-      column: 15,
-      func: 'foo',
-      line: 52,
-      url: 'http://path/to/file.js',
-    })
+    expect(stackFrames.stack[0]).toEqual({ args: [], column: 22, func: '?', line: 47, url: 'http://path/to/file.js' })
+    expect(stackFrames.stack[1]).toEqual({ args: [], column: 15, func: 'foo', line: 52, url: 'http://path/to/file.js' })
     expect(stackFrames.stack[2]).toEqual({
       args: [],
       column: 168,
@@ -1012,4 +870,92 @@ Error: foo
       url: 'capacitor://localhost/media/dist/bundle.js',
     })
   })
+
+  it('should parse Safari WebAssembly stack frames', () => {
+    const wasmStack = `
+  RuntimeError: Out of bounds memory access
+      myModule.wasm-function[crashHandler]@[wasm code]
+      <?>.wasm-function[42]@[wasm code]
+      handleError@http://192.168.1.113:3000/:98:25
+      @http://192.168.1.113:3000/:113:32
+    `
+    const mockErr = { message: 'Out of bounds memory access', name: 'RuntimeError', stack: wasmStack }
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(4)
+
+    expect(stackFrames.stack[0]).toEqual({
+      args: [],
+      func: 'myModule.wasm-function[crashHandler]',
+      url: '[wasm code]',
+      column: undefined,
+      line: undefined,
+    })
+
+    expect(stackFrames.stack[1]).toEqual({
+      args: [],
+      func: '<?>.wasm-function[42]',
+      url: '[wasm code]',
+      column: undefined,
+      line: undefined,
+    })
+  })
+
+  it('should parse Safari Wasm stack frames from file URL', () => {
+    const wasmStack = `
+  RuntimeError: Some Wasm Error
+      myModule.wasm-function[crashFunc]@[wasm code]
+      regularJsFunc@http://example.com/script.js:10:5
+    `
+    const mockErr = { message: 'Some Wasm Error', name: 'RuntimeError', stack: wasmStack }
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(2)
+    expect(stackFrames.stack[0]).toEqual({
+      args: [],
+      func: 'myModule.wasm-function[crashFunc]',
+      url: '[wasm code]',
+      line: undefined,
+      column: undefined,
+    })
+  })
+
+  it('should parse Chrome WebAssembly stack frames', () => {
+    const wasmStack = `
+  Error: Wasm Error
+    at myModule.foo (http://example.com/myModule.wasm:wasm-function[42]:0x1a3b)
+    at bar (http://example.com/script.js:10:5)
+    `
+    const mockErr = { message: 'Wasm Error', name: 'Error', stack: wasmStack }
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(2)
+    expect(stackFrames.stack[0]).toEqual({
+      args: [],
+      func: 'myModule.foo',
+      url: 'http://example.com/myModule.wasm:wasm-function[42]:0x1a3b',
+      line: undefined,
+      column: undefined,
+    })
+  })
+
+  it('should parse Firefox WebAssembly stack frames', () => {
+    const wasmStack = `
+  Error: Wasm Error
+    myModule.foo@http://example.com/my-module.wasm:wasm-function[42]:0x1a3b
+    bar@http://example.com/script.js:10:5
+    `
+    const mockErr = { message: 'Wasm Error', name: 'Error', stack: wasmStack }
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(2)
+
+    expect(stackFrames.stack[0]).toEqual({
+      args: [],
+      func: 'myModule.foo',
+      url: 'http://example.com/my-module.wasm:wasm-function[42]:0x1a3b',
+      line: undefined,
+      column: undefined,
+    })
+  })
 })
diff --git a/packages/core/src/tools/stackTrace/computeStackTrace.ts b/packages/core/src/tools/stackTrace/computeStackTrace.ts
index d9cf39cc56..040645ff17 100644
--- a/packages/core/src/tools/stackTrace/computeStackTrace.ts
+++ b/packages/core/src/tools/stackTrace/computeStackTrace.ts
@@ -125,7 +125,7 @@ function parseWinLine(line: string): StackFrame | undefined {
 }
 
 const GECKO_LINE_RE =
-  /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|capacitor|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i
+  /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|capacitor|\[native).*?|[^@]*bundle|\[wasm code\])(?::(\d+))?(?::(\d+))?\s*$/i
 const GECKO_EVAL_RE = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i
 
 function parseGeckoLine(line: string): StackFrame | undefined {


Reviews:
user: login: BenoitZugmeyer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T16:42:19Z
user: login: sethfowler-datadog
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:31:34Z
user: login: sethfowler-datadog
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T09:54:32Z
user: login: BenoitZugmeyer
body: I experimented with WASM stack frames and this is what I got:

| Browser | With debug info                                         | Without debug info                     |
| ------- | ------------------------------------------------------- | -------------------------------------- |
| Chrome  | `at MODULE.FUNCTION (URL:wasm-function[INDEX]:ADDRESS)` | `at URL:wasm-function[INDEX]:ADDRESS`  |
| Firefox | `MODULE.FUNCTION@URL:wasm-function[INDEX]:ADDRESS`      | `@URL:wasm-function[INDEX]:ADDRESS`    |
| Safari  | `MODULE.wasm-function[FUNCTION]@[wasm-code]`            | `<?>.wasm-function[INDEX]@[wasm-code]` |


I build a page to visualize the various stack traces: https://datadoghq.dev/browser-sdk-test-playground/wasm-crash-test/
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T16:38:36Z
user: login: BenoitZugmeyer
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-22T09:48:48Z
user: login: sethfowler-datadog
body: We discussed the particular formatting approach used here, and in particular, the plan to include the function index and PC offset in the URL. We concluded that, given the way stack traces are serialized and sent to the backend, this representation is a reasonable one.

So, LGTM! Thanks for your hard work on this one.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T14:09:10Z

Review comments:
diff_hunk: @@ -153,6 +153,16 @@ function parseGeckoLine(line: string): StackFrame | undefined {
   }
 }
 
+const SAFARI_WASM_RE = /^(\S+)\[(\S+)\]@\[wasm code\]$/i
+
+function parseSafariWasmLine(line: string): StackFrame | undefined {
+  const match = SAFARI_WASM_RE.exec(line.trim())
+  if (!match) {
+    return
+  }
+  return { func: `${match[1]}.${match[2]}`, url: '[wasm code]', args: [] }
+}
+
path: packages/core/src/tools/stackTrace/computeStackTrace.ts
user: login: BenoitZugmeyer
body: **suggestion:**  for better or worse, on safari, stack frames are parsed by the `GECKO_LINE_RE`. We could add support for the `[wasm code]` URL by changing that regex:

```diff
 const GECKO_LINE_RE =
-  /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|capacitor|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i
+  /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|capacitor|\[native).*?|[^@]*bundle|\[wasm code\])(?::(\d+))?(?::(\d+))?\s*$/i
```

It's more concise than having a dedicated function for it (although we might want to clean that regex one day :D).
created_at: 2025-04-15T16:39:39Z
author_association: MEMBER
diff_hunk: @@ -1012,4 +870,36 @@ Error: foo
       url: 'capacitor://localhost/media/dist/bundle.js',
     })
   })
+
+  it('should parse Safari WebAssembly stack frames', () => {
path: packages/core/src/tools/stackTrace/computeStackTrace.spec.ts
user: login: BenoitZugmeyer
body: **suggestion:** as mentioned during standup, it could be nice to add wasm stack traces tests for Chrome and Firefox as well
created_at: 2025-04-15T16:40:48Z
author_association: MEMBER
diff_hunk: @@ -1012,4 +870,36 @@ Error: foo
       url: 'capacitor://localhost/media/dist/bundle.js',
     })
   })
+
+  it('should parse Safari WebAssembly stack frames', () => {
+    const wasmStack = `
+  RuntimeError: Out of bounds memory access
+      myModule.wasm-function[crashHandler]@[wasm code]
+      handleError@https://example.com/app.js:123:45
+      @https://example.com/app.js:150:30
+    `
+    const mockErr = { message: 'Out of bounds memory access', name: 'RuntimeError', stack: wasmStack }
+
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(3)
+
+    expect(stackFrames.stack[0]).toEqual({ func: 'myModule.wasm-function.crashHandler', url: '[wasm code]', args: [] })
+
+    expect(stackFrames.stack[1]).toEqual({
+      func: 'handleError',
+      url: 'https://example.com/app.js',
+      line: 123,
+      column: 45,
+      args: [],
+    })
+
+    expect(stackFrames.stack[2]).toEqual({
+      func: '?',
+      url: 'https://example.com/app.js',
+      line: 150,
+      column: 30,
+      args: [],
+    })
path: packages/core/src/tools/stackTrace/computeStackTrace.spec.ts
user: login: BenoitZugmeyer
body: **suggestion:** Remove assertions on non-wasm stack frames, as it's unrelated to the test

```suggestion
```
created_at: 2025-04-15T16:41:58Z
author_association: MEMBER
diff_hunk: @@ -1012,4 +870,36 @@ Error: foo
       url: 'capacitor://localhost/media/dist/bundle.js',
     })
   })
+
+  it('should parse Safari WebAssembly stack frames', () => {
+    const wasmStack = `
+  RuntimeError: Out of bounds memory access
+      myModule.wasm-function[crashHandler]@[wasm code]
path: packages/core/src/tools/stackTrace/computeStackTrace.spec.ts
user: login: sethfowler-datadog
body: It might be worth including a stack frame that would test the case where no debug info was available for the wasm function. In that situation, I think that you'd get a stack frame that looks roughly like this on Safari:
```
myModule.wasm-function[42]@[wasm code]
```
In other words, you'd have the function's numeric index instead of its name.

I think both your regex and Benoît's proposed regex handle this just fine; it'd just be good to lock in that behavior with a test.
created_at: 2025-04-16T09:31:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1012,4 +870,65 @@ Error: foo
       url: 'capacitor://localhost/media/dist/bundle.js',
     })
   })
+
+  it('should parse Safari WebAssembly stack frames', () => {
+    const wasmStack = `
+  RuntimeError: Out of bounds memory access
+      myModule.wasm-function[crashHandler]@[wasm code]
+      handleError@https://example.com/app.js:123:45
+      @https://example.com/app.js:150:30
+    `
+    const mockErr = { message: 'Out of bounds memory access', name: 'RuntimeError', stack: wasmStack }
+
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(3)
+
+    expect(stackFrames.stack[0]).toEqual({
+      func: 'myModule.wasm-function[crashHandler]',
+      url: '[wasm code]',
+      column: undefined,
+      line: undefined,
+      args: [],
+    })
+  })
+
+  it('should parse Chrome WebAssembly stack frames', () => {
+    const wasmStack = `
+  Error: Wasm Error
+    at foo (<anonymous>:wasm-function[42]:0x1a3b)
+    at bar (http://example.com/script.js:10:5)
+    `
+    const mockErr = { message: 'Wasm Error', name: 'Error', stack: wasmStack }
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(2)
+    expect(stackFrames.stack[0]).toEqual({
+      args: [],
+      func: 'foo',
+      url: '<anonymous>:wasm-function[42]:0x1a3b',
+      line: undefined,
+      column: undefined,
+    })
+  })
+
+  it('should parse Firefox WebAssembly stack frames', () => {
+    const wasmStack = `
+  Error: Wasm Error
+    wasm-function[42]@http://example.com/my-module.wasm:wasm-function[42]:0x1a3b
+    bar@http://example.com/script.js:10:5
+    `
+    const mockErr = { message: 'Wasm Error', name: 'Error', stack: wasmStack }
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(2)
+
+    expect(stackFrames.stack[0]).toEqual({
+      args: [],
+      func: 'wasm-function[42]',
+      url: 'http://example.com/my-module.wasm:wasm-function[42]:0x1a3b',
path: packages/core/src/tools/stackTrace/computeStackTrace.spec.ts
user: login: sethfowler-datadog
body: This is a mis-parse; the URL here should just be:
```suggestion
      url: 'http://example.com/my-module.wasm',
```
created_at: 2025-04-16T09:48:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1012,4 +870,65 @@ Error: foo
       url: 'capacitor://localhost/media/dist/bundle.js',
     })
   })
+
+  it('should parse Safari WebAssembly stack frames', () => {
+    const wasmStack = `
+  RuntimeError: Out of bounds memory access
+      myModule.wasm-function[crashHandler]@[wasm code]
+      handleError@https://example.com/app.js:123:45
+      @https://example.com/app.js:150:30
+    `
+    const mockErr = { message: 'Out of bounds memory access', name: 'RuntimeError', stack: wasmStack }
+
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(3)
+
+    expect(stackFrames.stack[0]).toEqual({
+      func: 'myModule.wasm-function[crashHandler]',
+      url: '[wasm code]',
+      column: undefined,
+      line: undefined,
+      args: [],
+    })
+  })
+
+  it('should parse Chrome WebAssembly stack frames', () => {
+    const wasmStack = `
+  Error: Wasm Error
+    at foo (<anonymous>:wasm-function[42]:0x1a3b)
+    at bar (http://example.com/script.js:10:5)
+    `
+    const mockErr = { message: 'Wasm Error', name: 'Error', stack: wasmStack }
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(2)
+    expect(stackFrames.stack[0]).toEqual({
+      args: [],
+      func: 'foo',
+      url: '<anonymous>:wasm-function[42]:0x1a3b',
path: packages/core/src/tools/stackTrace/computeStackTrace.spec.ts
user: login: sethfowler-datadog
body: Same here; the URL is actually just `<anonymous>`:
```suggestion
      url: '<anonymous>',
```
We might want to represent it as the empty string instead, though.
created_at: 2025-04-16T09:49:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1012,4 +870,73 @@ Error: foo
       url: 'capacitor://localhost/media/dist/bundle.js',
     })
   })
+
+  it('should parse Safari WebAssembly stack frames', () => {
+    const wasmStack = `
+  RuntimeError: Out of bounds memory access
+      myModule.wasm-function[crashHandler]@[wasm code]
+      myModule.wasm-function[42]@[wasm code]
+      handleError@http://192.168.1.113:3000/:98:25
+      @http://192.168.1.113:3000/:113:32
+    `
+    const mockErr = { message: 'Out of bounds memory access', name: 'RuntimeError', stack: wasmStack }
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(4)
+
+    expect(stackFrames.stack[0]).toEqual({
+      args: [],
+      func: 'myModule.wasm-function[crashHandler]',
+      url: '[wasm code]',
path: packages/core/src/tools/stackTrace/computeStackTrace.spec.ts
user: login: sethfowler-datadog
body: We took this example from the reporter's test case, but the reporter's test case did the WASM equivalent of `eval()` (it embedded the WASM file as an `ArrayBuffer` of bytes), so there's no meaningful URL for the WASM module. It'd be nice to test what happens when you load the WASM module from a file, so that it _does_ have a URL. We should test that we parse that URL properly when it's present.

I suspect that `[wasm code]` is the equivalent of `<anonymous>` in the Chrome test case -- it may just be a stand-in for "no URL". It might be better to represent this case at the empty string, but whatever we do, we should align it across browsers so that stack traces from different browsers can be correctly merged.
created_at: 2025-04-16T09:54:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1012,4 +870,92 @@ Error: foo
       url: 'capacitor://localhost/media/dist/bundle.js',
     })
   })
+
+  it('should parse Safari WebAssembly stack frames', () => {
+    const wasmStack = `
+  RuntimeError: Out of bounds memory access
+      myModule.wasm-function[crashHandler]@[wasm code]
+      myModule.wasm-function[42]@[wasm code]
path: packages/core/src/tools/stackTrace/computeStackTrace.spec.ts
user: login: BenoitZugmeyer
body: **issue:** this stack frame is probably incorrect, it should be
```suggestion
      <?>.wasm-function[42]@[wasm code]
```
created_at: 2025-04-17T16:22:03Z
author_association: MEMBER
diff_hunk: @@ -1012,4 +870,92 @@ Error: foo
       url: 'capacitor://localhost/media/dist/bundle.js',
     })
   })
+
+  it('should parse Safari WebAssembly stack frames', () => {
+    const wasmStack = `
+  RuntimeError: Out of bounds memory access
+      myModule.wasm-function[crashHandler]@[wasm code]
+      myModule.wasm-function[42]@[wasm code]
+      handleError@http://192.168.1.113:3000/:98:25
+      @http://192.168.1.113:3000/:113:32
+    `
+    const mockErr = { message: 'Out of bounds memory access', name: 'RuntimeError', stack: wasmStack }
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(4)
+
+    expect(stackFrames.stack[0]).toEqual({
+      args: [],
+      func: 'myModule.wasm-function[crashHandler]',
+      url: '[wasm code]',
+      column: undefined,
+      line: undefined,
+    })
+
+    expect(stackFrames.stack[1]).toEqual({
+      args: [],
+      func: 'myModule.wasm-function[42]', // test case if have numeric index instead of the name
+      url: '[wasm code]',
+      column: undefined,
+      line: undefined,
+    })
+  })
+
+  it('should parse Safari Wasm stack frames from file URL', () => {
+    const wasmStack = `
+  RuntimeError: Some Wasm Error
+      myModule.wasm-function[crashFunc]@http://example.com/myModule.wasm
path: packages/core/src/tools/stackTrace/computeStackTrace.spec.ts
user: login: BenoitZugmeyer
body: **issue:** this stack frame is probably incorrect, from my test Safari never includes the .wasm URL

```suggestion
      myModule.wasm-function[crashFunc]@[wasm code]
```
created_at: 2025-04-17T16:23:01Z
author_association: MEMBER
diff_hunk: @@ -1012,4 +870,92 @@ Error: foo
       url: 'capacitor://localhost/media/dist/bundle.js',
     })
   })
+
+  it('should parse Safari WebAssembly stack frames', () => {
+    const wasmStack = `
+  RuntimeError: Out of bounds memory access
+      myModule.wasm-function[crashHandler]@[wasm code]
+      myModule.wasm-function[42]@[wasm code]
+      handleError@http://192.168.1.113:3000/:98:25
+      @http://192.168.1.113:3000/:113:32
+    `
+    const mockErr = { message: 'Out of bounds memory access', name: 'RuntimeError', stack: wasmStack }
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(4)
+
+    expect(stackFrames.stack[0]).toEqual({
+      args: [],
+      func: 'myModule.wasm-function[crashHandler]',
+      url: '[wasm code]',
+      column: undefined,
+      line: undefined,
+    })
+
+    expect(stackFrames.stack[1]).toEqual({
+      args: [],
+      func: 'myModule.wasm-function[42]', // test case if have numeric index instead of the name
+      url: '[wasm code]',
+      column: undefined,
+      line: undefined,
+    })
+  })
+
+  it('should parse Safari Wasm stack frames from file URL', () => {
+    const wasmStack = `
+  RuntimeError: Some Wasm Error
+      myModule.wasm-function[crashFunc]@http://example.com/myModule.wasm
+      regularJsFunc@http://example.com/script.js:10:5
+    `
+    const mockErr = { message: 'Some Wasm Error', name: 'RuntimeError', stack: wasmStack }
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(2)
+    expect(stackFrames.stack[0]).toEqual({
+      args: [],
+      func: 'myModule.wasm-function[crashFunc]',
+      url: 'http://example.com/myModule.wasm',
+      line: undefined,
+      column: undefined,
+    })
+  })
+
+  it('should parse Chrome WebAssembly stack frames', () => {
+    const wasmStack = `
+  Error: Wasm Error
+    at foo (<anonymous>:wasm-function[42]:0x1a3b)
path: packages/core/src/tools/stackTrace/computeStackTrace.spec.ts
user: login: BenoitZugmeyer
body: **issue:** this stack frame is probably incorrect, it should be something like:

```suggestion
    at myModule.foo (http://example.com/myModule.wasm:wasm-function[42]:0x1a3b)
```

When compiled without debug symbols, it becomes:

```
  at http://example.com/myModule.wasm:wasm-function[42]:0x1a3b
```
created_at: 2025-04-17T16:29:35Z
author_association: MEMBER
diff_hunk: @@ -1012,4 +870,92 @@ Error: foo
       url: 'capacitor://localhost/media/dist/bundle.js',
     })
   })
+
+  it('should parse Safari WebAssembly stack frames', () => {
+    const wasmStack = `
+  RuntimeError: Out of bounds memory access
+      myModule.wasm-function[crashHandler]@[wasm code]
+      myModule.wasm-function[42]@[wasm code]
+      handleError@http://192.168.1.113:3000/:98:25
+      @http://192.168.1.113:3000/:113:32
+    `
+    const mockErr = { message: 'Out of bounds memory access', name: 'RuntimeError', stack: wasmStack }
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(4)
+
+    expect(stackFrames.stack[0]).toEqual({
+      args: [],
+      func: 'myModule.wasm-function[crashHandler]',
+      url: '[wasm code]',
+      column: undefined,
+      line: undefined,
+    })
+
+    expect(stackFrames.stack[1]).toEqual({
+      args: [],
+      func: 'myModule.wasm-function[42]', // test case if have numeric index instead of the name
+      url: '[wasm code]',
+      column: undefined,
+      line: undefined,
+    })
+  })
+
+  it('should parse Safari Wasm stack frames from file URL', () => {
+    const wasmStack = `
+  RuntimeError: Some Wasm Error
+      myModule.wasm-function[crashFunc]@http://example.com/myModule.wasm
+      regularJsFunc@http://example.com/script.js:10:5
+    `
+    const mockErr = { message: 'Some Wasm Error', name: 'RuntimeError', stack: wasmStack }
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(2)
+    expect(stackFrames.stack[0]).toEqual({
+      args: [],
+      func: 'myModule.wasm-function[crashFunc]',
+      url: 'http://example.com/myModule.wasm',
+      line: undefined,
+      column: undefined,
+    })
+  })
+
+  it('should parse Chrome WebAssembly stack frames', () => {
+    const wasmStack = `
+  Error: Wasm Error
+    at foo (<anonymous>:wasm-function[42]:0x1a3b)
+    at bar (http://example.com/script.js:10:5)
+    `
+    const mockErr = { message: 'Wasm Error', name: 'Error', stack: wasmStack }
+    const stackFrames = computeStackTrace(mockErr)
+
+    expect(stackFrames.stack.length).toBe(2)
+    expect(stackFrames.stack[0]).toEqual({
+      args: [],
+      func: 'foo',
+      url: '<anonymous>',
+      line: undefined,
+      column: undefined,
+    })
+  })
+
+  it('should parse Firefox WebAssembly stack frames', () => {
+    const wasmStack = `
+  Error: Wasm Error
+    wasm-function[42]@http://example.com/my-module.wasm:wasm-function[42]:0x1a3b
path: packages/core/src/tools/stackTrace/computeStackTrace.spec.ts
user: login: BenoitZugmeyer
body: **issue:** this stack frame is probably incorrect, it should be

```
myModule.foo@http://example.com/my-module.wasm:wasm-function[42]:0x1a3b
```

or without debug info:

```
@http://example.com/my-module.wasm:wasm-function[42]:0x1a3b
```
created_at: 2025-04-17T16:31:32Z
author_association: MEMBER
diff_hunk: @@ -183,3 +181,16 @@ function tryToParseMessage(messageObj: unknown) {
   }
   return { name, message }
 }
+
+const WASM_STANDARD_URL_RE = /^(.+?):wasm-function\[.+?\]:0x[a-fA-F0-9]+$/
+
+function tryToParseWasmUrl(frame: StackFrame): void {
+  if (frame.url) {
+    const wasmUrlMatch = WASM_STANDARD_URL_RE.exec(frame.url)
+    if (wasmUrlMatch) {
+      frame.url = wasmUrlMatch[1]
+      frame.line = undefined
+      frame.column = undefined
+    }
+  }
+}
path: packages/core/src/tools/stackTrace/computeStackTrace.ts
user: login: BenoitZugmeyer
body: **suggestion:** we shouldn't be too opinionated and avoid hiding information that could be helpful to the customer. In this case:
* the `wasm-function` mention is valuable as it helps to understand the stack frame comes from wasm
* the function index is valuable when there is no debug information to help identify which function was called
* the address is valuable to identify the location of the stack frame within the function

So, I suggest keeping the whole "URL".
created_at: 2025-04-17T16:38:12Z
author_association: MEMBER

PR comments:
user: login: codecov-commenter
created_at: 2025-04-14T13:58:01Z
body: ## [Codecov](https://app.codecov.io/gh/DataDog/browser-sdk/pull/3481?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 91.99%. Comparing base [(`605b0d8`)](https://app.codecov.io/gh/DataDog/browser-sdk/commit/605b0d8c56e5b647ab96adb77e2a6d3ebe851f8d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog) to head [(`bb3a372`)](https://app.codecov.io/gh/DataDog/browser-sdk/commit/bb3a372a2889cbfe65da211cdbdad553f741ee83?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog).
> Report is 17 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #3481   +/-   ##
=======================================
  Coverage   91.99%   91.99%           
=======================================
  Files         311      311           
  Lines        8031     8031           
  Branches     1817     1817           
=======================================
  Hits         7388     7388           
  Misses        643      643           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/DataDog/browser-sdk/pull/3481?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: cit-pr-commenter[bot]
created_at: 2025-04-14T14:12:35Z
body: ## Bundles Sizes Evolution

| 📦 Bundle Name | Base Size | Local Size | 𝚫 | 𝚫% | Status |
| --- | --- | --- | --- | --- | --- |
| Rum | 144.96 KiB | 144.98 KiB | 14 B | +0.01% | ✅ |
| Logs | 50.16 KiB | 50.18 KiB | 14 B | +0.03% | ✅ |
| Rum Slim | 104.50 KiB | 104.51 KiB | 14 B | +0.01% | ✅ |
| Worker | 23.59 KiB | 23.59 KiB | 0 B | 0.00% | ✅ |
</details>

<details>
<summary>🚀 CPU Performance</summary>

| Action Name | Base Average Cpu Time (ms) | Local Average Cpu Time (ms) | 𝚫 |
| --- | --- | --- | --- |
| addglobalcontext | 0.002 | 0.002 | 0.000 |
| addaction | 0.036 | 0.038 | 0.002 |
| addtiming | 0.001 | 0.001 | -0.000 |
| adderror | 0.039 | 0.039 | 0.000 |
| startstopsessionreplayrecording | 0.007 | 0.012 | 0.005 |
| startview | 0.394 | 0.394 | 0.001 |
| logmessage | 0.020 | 0.021 | 0.000 |

</details>

<details>
<summary>🧠 Memory Performance</summary>

| Action Name | Base Consumption Memory (bytes) | Local Consumption Memory (bytes) | 𝚫 (bytes) |
| --- | --- | --- | --- |
| addglobalcontext | 29.19 KiB | 25.92 KiB | -3354 B |
| addaction | 59.82 KiB | 54.23 KiB | -5724 B |
| addtiming | 25.64 KiB | 24.35 KiB | -1325 B |
| adderror | 60.19 KiB | 55.18 KiB | -5129 B |
| startstopsessionreplayrecording | 23.37 KiB | 22.89 KiB | -493 B |
| startview | 428.38 KiB | 426.17 KiB | -2263 B |
| logmessage | 61.22 KiB | 56.85 KiB | -4471 B |

</details>

🔗 [RealWorld](https://datadoghq.dev/browser-sdk-test-playground/realworld-scenario/?prNumber=3481)


author_association: NONE
user: login: cit-pr-commenter[bot]
created_at: 2025-04-17T09:59:39Z
body: ## Bundles Sizes Evolution

| 📦 Bundle Name | Base Size | Local Size | 𝚫 | 𝚫% | Status |
| --- | --- | --- | --- | --- | --- |
| Rum | 144.96 KiB | 144.98 KiB | 14 B | +0.01% | ✅ |
| Logs | 50.16 KiB | 50.18 KiB | 14 B | +0.03% | ✅ |
| Rum Slim | 104.50 KiB | 104.51 KiB | 14 B | +0.01% | ✅ |
| Worker | 23.59 KiB | 23.59 KiB | 0 B | 0.00% | ✅ |
</details>

<details>
<summary>🚀 CPU Performance</summary>

| Action Name | Base Average Cpu Time (ms) | Local Average Cpu Time (ms) | 𝚫 |
| --- | --- | --- | --- |
| addglobalcontext | 0.002 | 0.007 | 0.004 |
| addaction | 0.036 | 0.156 | 0.119 |
| addtiming | 0.001 | 0.003 | 0.002 |
| adderror | 0.039 | 0.092 | 0.053 |
| startstopsessionreplayrecording | 0.007 | 0.026 | 0.018 |
| startview | 0.394 | 0.604 | 0.210 |
| logmessage | 0.020 | 0.027 | 0.007 |

</details>

<details>
<summary>🧠 Memory Performance</summary>

| Action Name | Base Consumption Memory (bytes) | Local Consumption Memory (bytes) | 𝚫 (bytes) |
| --- | --- | --- | --- |
| addglobalcontext | 29.19 KiB | 27.44 KiB | -1791 B |
| addaction | 59.82 KiB | 54.52 KiB | -5430 B |
| addtiming | 25.64 KiB | 25.37 KiB | -279 B |
| adderror | 60.19 KiB | 60.88 KiB | 701 B |
| startstopsessionreplayrecording | 23.37 KiB | 23.99 KiB | 639 B |
| startview | 428.38 KiB | 425.02 KiB | -3434 B |
| logmessage | 61.22 KiB | 56.91 KiB | -4410 B |

</details>

🔗 [RealWorld](https://datadoghq.dev/browser-sdk-test-playground/realworld-scenario/?prNumber=3481)


author_association: NONE