Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from 'node:assert/strict';
import { PrismaClient } from '@prisma/client';
import * as Sentry from '@sentry/node';
import { randomBytes } from 'crypto';
Expand All @@ -11,6 +12,11 @@ async function run() {
op: 'transaction',
},
async span => {
await client.$connect();
await Sentry.withActiveSpan(null, async () => {
assert.deepEqual(await client.$queryRawUnsafe('SELECT 1 AS before_parent'), [{ before_parent: 1 }]);
});

await client.user.create({
data: {
name: 'Tilda',
Expand Down Expand Up @@ -40,6 +46,10 @@ async function run() {
},
});

await Sentry.withActiveSpan(null, async () => {
assert.deepEqual(await client.$queryRawUnsafe('SELECT 3 AS after_parent'), [{ after_parent: 3 }]);
});

setTimeout(async () => {
span.end();
await client.$disconnect();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const ADDITIONAL_DEPENDENCIES = {
function expectPrismaV5Spans(container: SerializedStreamedSpanContainer): void {
const segment = container.items.find(span => span.is_segment);
expect(segment?.name).toBe('Test Transaction');
expect(container.items.filter(span => span.is_segment)).toHaveLength(1);
const spans = container.items.filter(span => !span.is_segment);
expect(spans.length).toBeGreaterThanOrEqual(5);

Expand Down Expand Up @@ -148,7 +149,7 @@ describeWithDockerCompose('Prisma ORM v5', { workingDirectory: [__dirname] }, ()
'scenario.mjs',
'instrument.mjs',
(createRunner, test) => {
test('should instrument PostgreSQL queries from Prisma ORM', { timeout: 75_000 }, async () => {
test('only instruments PostgreSQL queries with an active parent span', { timeout: 75_000 }, async () => {
await createRunner().expect({ span: expectPrismaV5Spans }).start().completed();
});
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from 'node:assert/strict';
import { PrismaClient } from '@prisma/client';
import * as Sentry from '@sentry/node';
import { randomBytes } from 'crypto';
Expand All @@ -11,6 +12,11 @@ async function run() {
async span => {
const client = new PrismaClient();

await client.$connect();
await Sentry.withActiveSpan(null, async () => {
assert.deepEqual(await client.$queryRawUnsafe('SELECT 1 AS before_parent'), [{ before_parent: 1 }]);
});

await client.user.create({
data: {
name: 'Tilda',
Expand All @@ -28,6 +34,10 @@ async function run() {
},
});

await Sentry.withActiveSpan(null, async () => {
assert.deepEqual(await client.$queryRawUnsafe('SELECT 3 AS after_parent'), [{ after_parent: 3 }]);
});

setTimeout(async () => {
span.end();
await client.$disconnect();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ describeWithDockerCompose('Prisma ORM v6 Tests', { workingDirectory: [__dirname]
'scenario.mjs',
'instrument.mjs',
(createRunner, test) => {
test('should instrument PostgreSQL queries from Prisma ORM', { timeout: 75_000 }, async () => {
test('only instruments PostgreSQL queries with an active parent span', { timeout: 75_000 }, async () => {
await createRunner()
.expect({
span: container => {
const segment = container.items.find(span => span.is_segment);
expect(segment?.name).toBe('Test Transaction');
expect(container.items.filter(span => span.is_segment)).toHaveLength(1);

const spans = container.items.filter(span => !span.is_segment);
expect(spans.length).toBeGreaterThanOrEqual(5);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ export class ActiveTracingHelper implements TracingHelper {
}

public dispatchEngineSpans(spans: EngineSpan[]): void {
if (!getActiveSpan()) {
return;
}

const linkIds = new Map<string, string>();
const roots = spans.filter(span => span.parentId === null);

Expand Down Expand Up @@ -291,6 +295,9 @@ export class ActiveTracingHelper implements TracingHelper {
}

const parentSpan = getActiveSpan();
if (!parentSpan) {
return callback();
}
Comment thread
cursor[bot] marked this conversation as resolved.

const attributes = buildSpanAttributes(name, options.attributes);
const spanOptions = {
Expand Down
105 changes: 87 additions & 18 deletions packages/server-utils/test/integrations/prisma.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import type { Span } from '@sentry/core';
import { Client, createTransport, getCurrentScope, initAndBind, resolvedSyncPromise, spanToJSON } from '@sentry/core';
import { afterEach, describe, expect, it } from 'vitest';
import type { Client, Span, SpanAttributes } from '@sentry/core';
import {
createTransport,
getCurrentScope,
initAndBind,
resolvedSyncPromise,
spanToJSON,
startSpan,
withActiveSpan,
} from '@sentry/core';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { instrumentPrisma } from '../../src/integrations/prisma';
import type { TracingHelper } from '../../src/integrations/prisma/types';
import type { EngineSpan, TracingHelper } from '../../src/integrations/prisma/types';
import { TestClient } from '../mocks/client';

type PrismaGlobal = typeof globalThis & {
PRISMA_INSTRUMENTATION?: { helper?: TracingHelper };
Expand All @@ -13,18 +22,9 @@ function getHelper(): (TracingHelper & { createEngineSpan?: unknown }) | undefin
return (globalThis as PrismaGlobal).PRISMA_INSTRUMENTATION?.helper;
}

class TestClient extends Client<any> {
public eventFromException(): PromiseLike<any> {
return resolvedSyncPromise({});
}
public eventFromMessage(): PromiseLike<any> {
return resolvedSyncPromise({});
}
}

function initTestClient(): void {
function initTestClient(): Client {
getCurrentScope().setClient(undefined);
initAndBind(TestClient, {
return initAndBind(TestClient, {
dsn: 'https://username@domain/123',
integrations: [],
sendClientReports: false,
Expand All @@ -35,16 +35,19 @@ function initTestClient(): void {
}

/** Runs a `db_query` span through the installed helper and returns the span it created. */
function runDbQuerySpan(attributes: Record<string, unknown>): Span {
function runDbQuerySpan(attributes: SpanAttributes): Span {
let span: Span | undefined;
getHelper()?.runInChildSpan({ name: 'db_query', attributes }, createdSpan => {
span = createdSpan;
startSpan({ name: 'parent' }, () => {
getHelper()?.runInChildSpan({ name: 'db_query', attributes }, createdSpan => {
span = createdSpan;
});
});
return span!;
}

describe('instrumentPrisma', () => {
afterEach(() => {
getCurrentScope().setClient(undefined);
const g = globalThis as PrismaGlobal;
g.PRISMA_INSTRUMENTATION = undefined;
g.V7_PRISMA_INSTRUMENTATION = undefined;
Expand All @@ -71,6 +74,72 @@ describe('instrumentPrisma', () => {
expect(helper?.isEnabled()).toBe(true);
});

describe('engine spans', () => {
const engineSpans: EngineSpan[] = [
{
id: 'engine-query',
parentId: null,
name: 'prisma:engine:query',
kind: 'internal',
startTime: [1, 0],
endTime: [1, 1_000_000],
},
{
id: 'db-query',
parentId: 'engine-query',
name: 'prisma:engine:db_query',
kind: 'client',
startTime: [1, 0],
endTime: [1, 1_000_000],
attributes: {
'db.query.text': 'SELECT * FROM "User"',
'db.system.name': 'postgresql',
},
},
];

it('runs the callback without creating client or engine spans when no parent is active', () => {
const client = initTestClient();
instrumentPrisma();
const spanStart = vi.fn();
client.on('spanStart', spanStart);
const helper = getHelper()!;
const callback = vi.fn(() => {
helper.dispatchEngineSpans(engineSpans);
return 'query result';
});

const result = withActiveSpan(null, () => helper.runInChildSpan('operation', callback));

expect(result).toBe('query result');
expect(callback).toHaveBeenCalledExactlyOnceWith();
expect(spanStart).not.toHaveBeenCalled();
});

it('attaches the engine batch to the active parent and preserves its child relationships', () => {
const client = initTestClient();
instrumentPrisma();
const endedSpans: Span[] = [];
client.on('spanEnd', span => endedSpans.push(span));
const helper = getHelper()!;

startSpan({ name: 'parent' }, parent => {
helper.dispatchEngineSpans(engineSpans);

const engineSpan = endedSpans.find(span => spanToJSON(span).name === 'prisma:engine:query')!;
expect(
endedSpans.map(span => ({
name: spanToJSON(span).name,
parentSpanId: spanToJSON(span).parent_span_id,
})),
).toEqual([
{ name: 'SELECT "User"', parentSpanId: engineSpan.spanContext().spanId },
{ name: 'prisma:engine:query', parentSpanId: parent.spanContext().spanId },
]);
});
});
});

describe('db.query.summary', () => {
it('summarizes a standard-dialect statement', () => {
initTestClient();
Expand Down
Loading