Skip to content

Commit d3c0f13

Browse files
committed
feat(core): Add Supabase Queues support
1 parent b0a1ad2 commit d3c0f13

File tree

25 files changed

+2783
-40
lines changed

25 files changed

+2783
-40
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as Sentry from '@sentry/browser';
2+
import { createClient } from '@supabase/supabase-js';
3+
4+
window.Sentry = Sentry;
5+
6+
const supabaseClient = createClient('https://test.supabase.co', 'test-key');
7+
8+
Sentry.init({
9+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
10+
integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })],
11+
tracesSampleRate: 1.0,
12+
});
13+
14+
// Simulate generic RPC call
15+
async function callGenericRpc() {
16+
try {
17+
await supabaseClient.rpc('my_custom_function', { param1: 'value1' });
18+
} catch (error) {
19+
Sentry.captureException(error);
20+
}
21+
}
22+
23+
callGenericRpc();
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { Page } from '@playwright/test';
2+
import { expect } from '@playwright/test';
3+
import type { Event } from '@sentry/core';
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
6+
7+
async function mockSupabaseRoute(page: Page) {
8+
await page.route('**/rpc/my_custom_function', route => {
9+
return route.fulfill({
10+
status: 200,
11+
body: JSON.stringify({ result: 'success' }),
12+
headers: {
13+
'Content-Type': 'application/json',
14+
},
15+
});
16+
});
17+
}
18+
19+
const bundle = process.env.PW_BUNDLE || '';
20+
// We only want to run this in non-CDN bundle mode
21+
if (bundle.startsWith('bundle')) {
22+
sentryTest.skip();
23+
}
24+
25+
sentryTest(
26+
'should capture exactly one db span for generic RPC calls (no double instrumentation)',
27+
async ({ getLocalTestUrl, page }) => {
28+
if (shouldSkipTracingTest()) {
29+
return;
30+
}
31+
32+
await mockSupabaseRoute(page);
33+
34+
const url = await getLocalTestUrl({ testDir: __dirname });
35+
36+
const event = await getFirstSentryEnvelopeRequest<Event>(page, url);
37+
const dbSpans = event.spans?.filter(({ op }) => op === 'db');
38+
39+
// Should have exactly one db span (not doubled by PostgREST instrumentation)
40+
expect(dbSpans).toHaveLength(1);
41+
42+
expect(dbSpans![0]).toMatchObject({
43+
description: 'rpc(my_custom_function)',
44+
parent_span_id: event.contexts?.trace?.span_id,
45+
span_id: expect.any(String),
46+
start_timestamp: expect.any(Number),
47+
timestamp: expect.any(Number),
48+
trace_id: event.contexts?.trace?.trace_id,
49+
data: expect.objectContaining({
50+
'sentry.op': 'db',
51+
'sentry.origin': 'auto.db.supabase',
52+
'db.system': 'postgresql',
53+
'db.operation': 'insert',
54+
'db.table': 'my_custom_function',
55+
'db.params': { param1: 'value1' },
56+
}),
57+
});
58+
},
59+
);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as Sentry from '@sentry/browser';
2+
import { createClient } from '@supabase/supabase-js';
3+
4+
window.Sentry = Sentry;
5+
6+
const supabaseClient = createClient('https://test.supabase.co', 'test-key', {
7+
db: {
8+
schema: 'pgmq_public',
9+
},
10+
});
11+
12+
Sentry.init({
13+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
14+
integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })],
15+
tracesSampleRate: 1.0,
16+
});
17+
18+
// Simulate queue operations
19+
async function performQueueOperations() {
20+
try {
21+
await supabaseClient.rpc('send', {
22+
queue_name: 'todos',
23+
message: { title: 'Test Todo' },
24+
});
25+
26+
await supabaseClient.rpc('pop', {
27+
queue_name: 'todos',
28+
});
29+
} catch (error) {
30+
Sentry.captureException(error);
31+
}
32+
}
33+
34+
performQueueOperations();
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { Page } from '@playwright/test';
2+
import { expect } from '@playwright/test';
3+
import type { Event } from '@sentry/core';
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
6+
7+
async function mockSupabaseRoute(page: Page) {
8+
await page.route('**/rpc/send', route => {
9+
return route.fulfill({
10+
status: 200,
11+
body: JSON.stringify([0]),
12+
headers: {
13+
'Content-Type': 'application/json',
14+
},
15+
});
16+
});
17+
18+
await page.route('**/rpc/pop', route => {
19+
return route.fulfill({
20+
status: 200,
21+
body: JSON.stringify([
22+
{
23+
msg_id: 0,
24+
},
25+
]),
26+
headers: {
27+
'Content-Type': 'application/json',
28+
},
29+
});
30+
});
31+
}
32+
33+
const bundle = process.env.PW_BUNDLE || '';
34+
// We only want to run this in non-CDN bundle mode
35+
if (bundle.startsWith('bundle')) {
36+
sentryTest.skip();
37+
}
38+
39+
sentryTest('should capture Supabase queue spans from client.rpc', async ({ getLocalTestUrl, page }) => {
40+
if (shouldSkipTracingTest()) {
41+
return;
42+
}
43+
44+
await mockSupabaseRoute(page);
45+
46+
const url = await getLocalTestUrl({ testDir: __dirname });
47+
48+
const event = await getFirstSentryEnvelopeRequest<Event>(page, url);
49+
const queueSpans = event.spans?.filter(({ op }) => op?.startsWith('queue.'));
50+
51+
expect(queueSpans).toHaveLength(2);
52+
53+
expect(queueSpans![0]).toMatchObject({
54+
description: 'publish todos',
55+
parent_span_id: event.contexts?.trace?.span_id,
56+
span_id: expect.any(String),
57+
start_timestamp: expect.any(Number),
58+
timestamp: expect.any(Number),
59+
trace_id: event.contexts?.trace?.trace_id,
60+
data: expect.objectContaining({
61+
'sentry.op': 'queue.publish',
62+
'sentry.origin': 'auto.db.supabase.queue.producer',
63+
'messaging.destination.name': 'todos',
64+
'messaging.message.id': '0',
65+
}),
66+
});
67+
68+
expect(queueSpans![1]).toMatchObject({
69+
description: 'process todos',
70+
parent_span_id: event.contexts?.trace?.span_id,
71+
span_id: expect.any(String),
72+
start_timestamp: expect.any(Number),
73+
timestamp: expect.any(Number),
74+
trace_id: event.contexts?.trace?.trace_id,
75+
data: expect.objectContaining({
76+
'sentry.op': 'queue.process',
77+
'sentry.origin': 'auto.db.supabase.queue.consumer',
78+
'messaging.destination.name': 'todos',
79+
'messaging.message.id': '0',
80+
}),
81+
});
82+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as Sentry from '@sentry/browser';
2+
import { createClient } from '@supabase/supabase-js';
3+
4+
window.Sentry = Sentry;
5+
6+
const supabaseClient = createClient('https://test.supabase.co', 'test-key', {
7+
db: {
8+
schema: 'pgmq_public',
9+
},
10+
});
11+
12+
Sentry.init({
13+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
14+
integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })],
15+
tracesSampleRate: 1.0,
16+
});
17+
18+
// Simulate queue operations
19+
async function performQueueOperations() {
20+
try {
21+
await supabaseClient.rpc('pgmq.send', {
22+
queue_name: 'todos',
23+
message: { title: 'Test Todo' },
24+
});
25+
26+
await supabaseClient.rpc('pgmq.pop', {
27+
queue_name: 'todos',
28+
});
29+
} catch (error) {
30+
Sentry.captureException(error);
31+
}
32+
}
33+
34+
performQueueOperations();
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { Page } from '@playwright/test';
2+
import { expect } from '@playwright/test';
3+
import type { Event } from '@sentry/core';
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
6+
7+
async function mockSupabaseRoute(page: Page) {
8+
await page.route('**/rpc/pgmq.send', route => {
9+
return route.fulfill({
10+
status: 200,
11+
body: JSON.stringify([0]),
12+
headers: {
13+
'Content-Type': 'application/json',
14+
},
15+
});
16+
});
17+
18+
await page.route('**/rpc/pgmq.pop', route => {
19+
return route.fulfill({
20+
status: 200,
21+
body: JSON.stringify([
22+
{
23+
msg_id: 0,
24+
},
25+
]),
26+
headers: {
27+
'Content-Type': 'application/json',
28+
},
29+
});
30+
});
31+
}
32+
33+
const bundle = process.env.PW_BUNDLE || '';
34+
// We only want to run this in non-CDN bundle mode
35+
if (bundle.startsWith('bundle')) {
36+
sentryTest.skip();
37+
}
38+
39+
sentryTest('should capture Supabase queue spans from schema-qualified RPC names', async ({ getLocalTestUrl, page }) => {
40+
if (shouldSkipTracingTest()) {
41+
return;
42+
}
43+
44+
await mockSupabaseRoute(page);
45+
46+
const url = await getLocalTestUrl({ testDir: __dirname });
47+
48+
const event = await getFirstSentryEnvelopeRequest<Event>(page, url);
49+
const queueSpans = event.spans?.filter(({ op }) => op?.startsWith('queue.'));
50+
51+
expect(queueSpans).toHaveLength(2);
52+
53+
expect(queueSpans![0]).toMatchObject({
54+
description: 'publish todos',
55+
parent_span_id: event.contexts?.trace?.span_id,
56+
span_id: expect.any(String),
57+
start_timestamp: expect.any(Number),
58+
timestamp: expect.any(Number),
59+
trace_id: event.contexts?.trace?.trace_id,
60+
data: expect.objectContaining({
61+
'sentry.op': 'queue.publish',
62+
'sentry.origin': 'auto.db.supabase.queue.producer',
63+
'messaging.destination.name': 'todos',
64+
'messaging.message.id': '0',
65+
}),
66+
});
67+
68+
expect(queueSpans![1]).toMatchObject({
69+
description: 'process todos',
70+
parent_span_id: event.contexts?.trace?.span_id,
71+
span_id: expect.any(String),
72+
start_timestamp: expect.any(Number),
73+
timestamp: expect.any(Number),
74+
trace_id: event.contexts?.trace?.trace_id,
75+
data: expect.objectContaining({
76+
'sentry.op': 'queue.process',
77+
'sentry.origin': 'auto.db.supabase.queue.consumer',
78+
'messaging.destination.name': 'todos',
79+
'messaging.message.id': '0',
80+
}),
81+
});
82+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as Sentry from '@sentry/browser';
2+
import { createClient } from '@supabase/supabase-js';
3+
4+
window.Sentry = Sentry;
5+
6+
const supabaseClient = createClient('https://test.supabase.co', 'test-key', {
7+
db: {
8+
schema: 'pgmq_public',
9+
},
10+
});
11+
12+
Sentry.init({
13+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
14+
integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })],
15+
tracesSampleRate: 1.0,
16+
});
17+
18+
// Simulate queue operations
19+
async function performQueueOperations() {
20+
try {
21+
await supabaseClient.schema('pgmq_public').rpc('send', {
22+
queue_name: 'todos',
23+
message: { title: 'Test Todo' },
24+
});
25+
26+
await supabaseClient.schema('pgmq_public').rpc('pop', {
27+
queue_name: 'todos',
28+
});
29+
} catch (error) {
30+
Sentry.captureException(error);
31+
}
32+
}
33+
34+
performQueueOperations();

0 commit comments

Comments
 (0)