Skip to content

Commit 1b67eba

Browse files
authored
fix: improve stacktrace of errors generated by proxied Prisma methods (#484)
1 parent a078b23 commit 1b67eba

File tree

2 files changed

+105
-1
lines changed

2 files changed

+105
-1
lines changed

packages/runtime/src/enhancements/proxy.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ export class DefaultPrismaProxyHandler implements PrismaProxyHandler {
155155
}
156156
}
157157

158+
// a marker for filtering error stack trace
159+
const ERROR_MARKER = '__error_marker__';
160+
158161
/**
159162
* Makes a Prisma client proxy.
160163
*/
@@ -216,9 +219,71 @@ export function makeProxy<T extends PrismaProxyHandler>(
216219
return undefined;
217220
}
218221

219-
return makeHandler(target, prop);
222+
return createHandlerProxy(makeHandler(target, prop));
220223
},
221224
});
222225

223226
return proxy;
224227
}
228+
229+
// A proxy for capturing errors and processing stack trace
230+
function createHandlerProxy<T extends PrismaProxyHandler>(handler: T): T {
231+
return new Proxy(handler, {
232+
get(target, propKey) {
233+
const prop = target[propKey as keyof T];
234+
if (typeof prop !== 'function') {
235+
return prop;
236+
}
237+
238+
// eslint-disable-next-line @typescript-eslint/ban-types
239+
const origMethod = prop as Function;
240+
return async function (...args: any[]) {
241+
const _err = new Error(ERROR_MARKER);
242+
try {
243+
return await origMethod.apply(handler, args);
244+
} catch (err) {
245+
if (_err.stack && err instanceof Error) {
246+
(err as any).internalStack = err.stack;
247+
err.stack = cleanCallStack(_err.stack, propKey.toString(), err.message);
248+
}
249+
throw err;
250+
}
251+
};
252+
},
253+
});
254+
}
255+
256+
// Filter out @zenstackhq/runtime stack (generated by proxy) from stack trace
257+
function cleanCallStack(stack: string, method: string, message: string) {
258+
// message line
259+
let resultStack = `Error calling enhanced Prisma method \`${method}\`: ${message}`;
260+
261+
const lines = stack.split('\n');
262+
let foundMarker = false;
263+
264+
for (let i = 0; i < lines.length; i++) {
265+
const line = lines[i];
266+
267+
if (!foundMarker) {
268+
// find marker, then stack trace lines follow
269+
if (line.includes(ERROR_MARKER)) {
270+
foundMarker = true;
271+
}
272+
continue;
273+
}
274+
275+
// skip leading zenstack and anonymous lines
276+
if (line.includes('@zenstackhq/runtime') || line.includes('<anonymous>')) {
277+
continue;
278+
}
279+
280+
// capture remaining lines
281+
resultStack += lines
282+
.slice(i)
283+
.map((l) => '\n' + l)
284+
.join();
285+
break;
286+
}
287+
288+
return resultStack;
289+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
import path from 'path';
3+
4+
describe('Stack trace tests', () => {
5+
let origDir: string;
6+
7+
beforeAll(async () => {
8+
origDir = path.resolve('.');
9+
});
10+
11+
afterEach(() => {
12+
process.chdir(origDir);
13+
});
14+
15+
it('stack trace', async () => {
16+
const { withPolicy } = await loadSchema(
17+
`
18+
model Model {
19+
id String @id @default(uuid())
20+
}
21+
`
22+
);
23+
24+
const db = withPolicy();
25+
let error: Error | undefined = undefined;
26+
27+
try {
28+
await db.model.create({ data: {} });
29+
} catch (err) {
30+
error = err as Error;
31+
}
32+
33+
expect(error?.stack).toContain(
34+
"Error calling enhanced Prisma method `create`: denied by policy: model entities failed 'create' check"
35+
);
36+
expect(error?.stack).toContain(`misc/stacktrace.test.ts`);
37+
expect((error as any).internalStack).toBeTruthy();
38+
});
39+
});

0 commit comments

Comments
 (0)