Skip to content

Commit b05ecaf

Browse files
authored
feat: support sourcemap resolution for *ESM* stack frames in captured APM errors (#4578)
1 parent 102b60a commit b05ecaf

File tree

7 files changed

+53
-25
lines changed

7 files changed

+53
-25
lines changed

docs/reference/starting-agent.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,12 @@ apm.start({
8080
serverUrl: 'https://...',
8181
secretToken: '...',
8282
// ...
83-
})
83+
});
8484
```
8585

8686
```ts
8787
// main.ts
88-
import 'initapm'
88+
import './initapm.js';
8989

9090
// Application code starts here.
9191
```

docs/release-notes/index.md

+11
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ To check for security updates, go to [Security announcements for the Elastic sta
2020

2121
% ### Fixes [next-fixes]
2222

23+
## Next [next]
24+
25+
### Features and enhancements [next-features-enhancements]
26+
27+
* Get sourcemap handling for captured exceptions to work with stack frames in
28+
ES Modules (ESM). Before this, sourcemap handling would only work for stack
29+
frames in CommonJS modules.
30+
31+
### Fixes [next-fixes]
32+
33+
2334
## 4.11.2 [4-11-2]
2435
**Release date:** March 17, 2025
2536

examples/typescript/README.md

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
This directory includes an example TypeScript project using the Elastic
2-
Node.js APM agent. It uses a tsconfig as recommended at
3-
https://github.com/tsconfig/bases#node-14-tsconfigjson
1+
This directory includes an example TypeScript project using the Elastic Node.js
2+
APM agent. It uses a tsconfig as recommended at https://github.com/tsconfig/bases#node-20-tsconfigjson
3+
and because `"type": "module"` is set in package.json, the built JavaScript will
4+
use ES Modules (i.e. `import`).
45

56
Install dependencies:
67

78
npm install
89

9-
Compile the TypeScript ("index.ts") to JavaScript ("dist/index.js"):
10+
Compile the TypeScript to JavaScript ("dist/..."):
1011

1112
npm run build
1213

@@ -15,7 +16,7 @@ the top of "index.ts". (See [the docs](https://www.elastic.co/guide/en/apm/agent
1516
for other ways of starting the APM agent.)
1617

1718
```ts
18-
import 'elastic-apm-node/start'
19+
import 'elastic-apm-node/start.js'
1920
```
2021

2122
This start methods means that we need to use environment variables (or an
@@ -26,7 +27,7 @@ Configure the APM agent with values from [your Elastic Stack](https://www.elasti
2627
export ELASTIC_APM_SERVER_URL='https://...apm...cloud.es.io:443'
2728
export ELASTIC_APM_SECRET_TOKEN='...'
2829
export ELASTIC_APM_USE_PATH_AS_TRANSACTION_NAME=true
29-
node dist/index.js
30+
node --experimental-loader=elastic-apm-node/loader.mjs dist/index.js
3031

3132
This simple script creates an HTTP server and makes a single request to it.
3233
If things work properly, you should see a trace with a single HTTP transaction

examples/typescript/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
* compliance with the BSD 2-Clause License.
55
*/
66

7-
// Be sure to import and *start* the agent before other imports.
8-
import 'elastic-apm-node/start'
7+
// Be sure to import and *start* the APM agent before other imports.
8+
import 'elastic-apm-node/start.js'
99

10-
import http from 'http'
10+
import * as http from 'http'
1111

1212
// Create an HTTP server listening at port 3000.
1313
const server = http.createServer((req, res) => {

examples/typescript/package.json

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
{
22
"name": "elastic-apm-node-typescript-example",
3-
"version": "1.0.0",
3+
"version": "2.0.0",
44
"private": true,
5-
"main": "index.ts",
5+
"type": "module",
66
"scripts": {
7-
"build": "tsc"
7+
"build": "tsc",
8+
"start": "node --enable-source-maps --experimental-loader=elastic-apm-node/loader.mjs dist/index.js"
89
},
910
"dependencies": {
10-
"elastic-apm-node": "^3.37.0"
11+
"elastic-apm-node": "^4.11.2"
1112
},
1213
"devDependencies": {
13-
"@tsconfig/node14": "^1.0.3",
14-
"typescript": "^4.7.4"
14+
"@tsconfig/node20": "^20.1.5",
15+
"typescript": "^5.0.4"
1516
}
1617
}

examples/typescript/tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"extends": "@tsconfig/node14/tsconfig.json",
2+
"extends": "@tsconfig/node20/tsconfig.json",
33
"compilerOptions": {
44
"outDir": "dist"
55
}

lib/stacktraces.js

+22-7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
var fsPromises = require('fs/promises');
1717
var path = require('path');
1818
var { promisify } = require('util');
19+
const { fileURLToPath } = require('url');
1920

2021
// avoid loading error-callsites until needed to avoid
2122
// Error.prepareStackTrace side-effects
@@ -89,6 +90,20 @@ function getCwd(log) {
8990
return cwd;
9091
}
9192

93+
// "filePath" refers to frame.fileName(), but with the possible "file://..."
94+
// URL converted to a local path. An callsite in an ES module will have a
95+
// file URL for the `fileName`.
96+
//
97+
// This just relies on `callsite.getFileName() -> <string | null | undefined>`
98+
// so it works with CallSite or StackFrames (from `error-stack-parser`).
99+
function filePathFromCallSite(callsite) {
100+
let filePath = callsite.getFileName();
101+
if (filePath && filePath.startsWith('file://')) {
102+
filePath = fileURLToPath(filePath);
103+
}
104+
return filePath;
105+
}
106+
92107
// If gathering a stacktrace from the structured CallSites fails, this is
93108
// used as a fallback: parsing the `err.stack` *string*.
94109
function stackTraceFromErrStackString(log, err) {
@@ -114,7 +129,7 @@ function stackTraceFromErrStackString(log, err) {
114129
const cwd = getCwd(log);
115130
for (var i = 0; i < frames.length; i++) {
116131
const frame = frames[i];
117-
const filename = frame.getFileName() || '';
132+
const filename = filePathFromCallSite(frame) || '';
118133
stacktrace.push({
119134
filename: getRelativeFileName(filename, cwd),
120135
function: frame.getFunctionName(),
@@ -136,7 +151,7 @@ function isStackFrameApp(stackframe) {
136151
if (isStackFrameNode(stackframe)) {
137152
return false;
138153
} else {
139-
const fileName = stackframe.getFileName();
154+
const fileName = filePathFromCallSite(stackframe);
140155
if (!fileName) {
141156
return true;
142157
} else if (fileName.indexOf(NODE_MODULES_PATH_SEG) === -1) {
@@ -153,7 +168,7 @@ function isStackFrameNode(stackframe) {
153168
if (stackframe.isNative) {
154169
return true;
155170
} else {
156-
const fileName = stackframe.getFileName();
171+
const fileName = filePathFromCallSite(stackframe);
157172
if (!fileName) {
158173
return true;
159174
} else {
@@ -166,7 +181,7 @@ function isCallSiteApp(callsite) {
166181
if (isCallSiteNode(callsite)) {
167182
return false;
168183
} else {
169-
const fileName = callsite.getFileName();
184+
const fileName = filePathFromCallSite(callsite);
170185
if (!fileName) {
171186
return true;
172187
} else if (fileName.indexOf(NODE_MODULES_PATH_SEG) === -1) {
@@ -181,7 +196,7 @@ function isCallSiteNode(callsite) {
181196
if (callsite.isNative()) {
182197
return true;
183198
} else {
184-
const fileName = callsite.getFileName();
199+
const fileName = filePathFromCallSite(callsite);
185200
if (!fileName) {
186201
return true;
187202
} else {
@@ -244,7 +259,7 @@ async function getSourceMapConsumer(callsite) {
244259
if (isCallSiteNode(callsite)) {
245260
return null;
246261
} else {
247-
var filename = callsite.getFileName();
262+
var filename = filePathFromCallSite(callsite);
248263
if (!filename) {
249264
return null;
250265
} else {
@@ -277,7 +292,7 @@ async function frameFromCallSite(
277292
sourceLinesLibraryFrames,
278293
) {
279294
// getFileName can return null, e.g. with a `at Generator.next (<anonymous>)` frame.
280-
const filename = callsite.getFileName() || '';
295+
const filename = filePathFromCallSite(callsite) || '';
281296
const lineno = callsite.getLineNumber();
282297
const colno = callsite.getColumnNumber();
283298

0 commit comments

Comments
 (0)