Skip to content
Merged
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,5 +1,10 @@
import { expect, test } from '@playwright/test';
import { collectStreamedSpansUntilSegment, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
import {
collectStreamedSpansUntilSegment,
getSpanOp,
waitForEnvelopeItem,
waitForStreamedSpan,
} from '@sentry-internal/test-utils';

type StreamedSpan = Awaited<ReturnType<typeof waitForStreamedSpan>>;

Expand Down Expand Up @@ -65,7 +70,21 @@ test('Sends client-side Supabase db-operation spans to Sentry', async ({ page })
);
});

const insertSpanPromise = waitForStreamedSpan('supabase-nextjs', span => span.name === 'insert todos');
let insertSpanSent = false;
waitForStreamedSpan('supabase-nextjs', span => {
if (span.name === 'insert todos') {
insertSpanSent = true;
}
return false;
}).catch(() => {});

const clientReportPromise = waitForEnvelopeItem('supabase-nextjs', ([header, payload]) => {
if (header.type !== 'client_report') {
return false;
}
const { discarded_events } = payload as { discarded_events: Array<{ reason: string; category: string }> };
return discarded_events.some(e => e.reason === 'no_parent_span' && e.category === 'span');
});

await page.goto('/');

Expand All @@ -78,29 +97,32 @@ test('Sends client-side Supabase db-operation spans to Sentry', async ({ page })
// Wait for login to complete
await page.waitForSelector('button:has-text("Add")');

const [pageloadSpan, selectSpan] = await Promise.all([pageloadSpanPromise, selectSpanPromise]);

// Add a new todo entry
await page.locator('input[id=new-task-text]').fill('test');
const task = `test-${Date.now()}`;
await page.locator('input[id=new-task-text]').fill(task);
await page.locator('button[id=add-task]').click();
await page.getByText(task).waitFor();

// Hiding the page flushes the pending client report.
await page.evaluate(() => {
Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'hidden' });
document.dispatchEvent(new Event('visibilitychange'));
});

const [pageloadSpan, selectSpan, insertSpan] = await Promise.all([
pageloadSpanPromise,
selectSpanPromise,
insertSpanPromise,
]);
await clientReportPromise;

// Client-side database query data is collected by default.
expectDbSpan(selectSpan, 'select todos', {
'db.operation.name': { value: 'select', type: 'string' },
'db.query': { value: ['select(*)', 'filter(order, asc)'], type: 'array' },
});
expect(selectSpan.trace_id).toBe(pageloadSpan.trace_id);
expect(selectSpan.parent_span_id).toBe(pageloadSpan.span_id);

// The insert is triggered long after the pageload span has ended, so it is streamed on its own
// rather than as a child of the pageload span.
expectDbSpan(insertSpan, 'insert todos', {
'db.operation.name': { value: 'insert', type: 'string' },
'db.query': { value: ['select(*)'], type: 'array' },
});
// The insert runs after the pageload span has ended, so it has no parent and no span is sent.
expect(insertSpanSent).toBe(false);
});

test('Sends server-side Supabase db-operation spans to Sentry', async ({ baseURL }) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/integrations/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): A
return startSpanManual(
{
name,
onlyIfParent: true,
attributes: {
[SENTRY_ORIGIN]: 'auto.db.supabase',
[SENTRY_OP]: DB,
Expand Down Expand Up @@ -479,6 +480,7 @@ function instrumentPostgRESTFilterBuilder(
return startSpanManual(
{
name,
onlyIfParent: true,
attributes,
},
span => {
Expand Down
38 changes: 38 additions & 0 deletions packages/core/test/lib/integrations/supabase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,4 +645,42 @@ describe('Supabase Integration', () => {
expect(spanOptions.attributes['db.operation.name']).toBe('upsert');
});
});

describe('onlyIfParent', () => {
beforeEach(() => {
vi.spyOn(exportsModule, 'captureException').mockImplementation(() => '');
vi.spyOn(breadcrumbModule, 'addBreadcrumb').mockImplementation(() => {});
});

afterEach(() => {
vi.restoreAllMocks();
});

it('starts the PostgREST span with onlyIfParent so it is dropped without an active parent', async () => {
tracingMocks.startSpanManual.mockClear();
const client = createMockSupabaseClient({ status: 200, data: [] });
instrumentSupabaseClient(client);

await (client as any).from('todos').select('*').then();

expect(tracingMocks.startSpanManual).toHaveBeenCalledWith(
expect.objectContaining({ onlyIfParent: true }),
expect.any(Function),
);
});

it('starts the auth span with onlyIfParent so it is dropped without an active parent', async () => {
tracingMocks.startSpanManual.mockClear();
const client = createMockSupabaseClient(undefined) as any;
client.auth.signInWithPassword = vi.fn(() => Promise.resolve({ data: { user: {} }, error: null }));
instrumentSupabaseClient(client);

await client.auth.signInWithPassword({ email: 'a@b.c', password: 'pw' });

expect(tracingMocks.startSpanManual).toHaveBeenCalledWith(
expect.objectContaining({ onlyIfParent: true }),
expect.any(Function),
);
});
});
});
Loading