-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix(cloudflare): Skip spans for Cloudflare-internal Durable Object SQL queries #22376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { stringMatchesSomePattern } from '@sentry/core'; | ||
|
|
||
| /** | ||
| * Cloudflare frameworks that build on Durable Objects (`agents`, `partyserver`, ...) manage their | ||
| * own internal SQLite tables, all namespaced with a `cf_` prefix — e.g. `cf_agents_schedules`, | ||
| * `cf_agent_state`, `cf_ai_chat_stream_chunks`. Queries against them (schedule polling, chat-stream | ||
| * persistence, state bookkeeping) are framework implementation details that otherwise flood traces | ||
| * with dozens of zero-signal `db.query` spans per request. The exact set of tables even varies | ||
| * between framework versions, so we match the reserved prefix rather than an enumerated list. | ||
| * | ||
| * The `cf_` prefix is a reserved convention for framework-managed tables, so user tables should not | ||
| * use it. In case a user table does collide with the prefix, the `durableObjectSqlSpanAllowlist` | ||
| * option lets them opt those tables back into instrumentation. | ||
| * | ||
| * The check operates on the query summary produced by `getSqlQuerySummary` (`{operation} {table} ...`, | ||
| * the same value used as the span name), so table targets are already isolated from the rest of the | ||
| * query. | ||
| */ | ||
| export function targetsCloudflareInternalTable( | ||
| querySummary: string | undefined, | ||
| allowlist?: Array<string | RegExp>, | ||
| ): boolean { | ||
| if (!querySummary) { | ||
| return false; | ||
| } | ||
|
|
||
| const [, ...tables] = querySummary.split(' '); | ||
|
|
||
| return tables.some(table => { | ||
| if (!table.toLowerCase().startsWith('cf_')) { | ||
| return false; | ||
| } | ||
|
|
||
| // A table on the allowlist is treated as a user table and stays instrumented, even though it | ||
| // matches the reserved prefix. | ||
| return !allowlist?.length || !stringMatchesSomePattern(table, allowlist, true); | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
106 changes: 106 additions & 0 deletions
106
packages/cloudflare/test/utils/internalSqlQuery.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import { _INTERNAL_getSqlQuerySummary } from '@sentry/core'; | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { targetsCloudflareInternalTable } from '../../src/utils/internalSqlQuery'; | ||
|
|
||
| // Builds the summary the same way `instrumentSqlStorage` does, so the test exercises the real | ||
| // operation -> summary -> detection path rather than hand-written summaries. | ||
| const summarize = (query: string): string | undefined => _INTERNAL_getSqlQuerySummary(query); | ||
|
|
||
| describe('targetsCloudflareInternalTable', () => { | ||
| describe('internal queries (cf_ tables)', () => { | ||
| it.each([ | ||
| ['SELECT', 'SELECT * FROM cf_agents_state WHERE id = ?'], | ||
| ['INSERT', 'INSERT INTO cf_agents_fibers (id, callback) VALUES (?, ?)'], | ||
| ['DELETE', 'DELETE FROM cf_agents_schedules WHERE id = ?'], | ||
| ['UPDATE', 'UPDATE cf_agent_tool_runs SET output_json = ? WHERE id = ?'], | ||
| ['CREATE TABLE', 'CREATE TABLE IF NOT EXISTS cf_agents_workflows (id TEXT PRIMARY KEY NOT NULL)'], | ||
| ['ALTER TABLE', 'ALTER TABLE cf_agents_queues ADD COLUMN retry_options TEXT'], | ||
| ['DROP TABLE', 'DROP TABLE cf_agents_state'], | ||
| ['cf_agent_ prefix', 'SELECT * FROM cf_agent_identity'], | ||
| ['cf_ai_ prefix', 'INSERT INTO cf_ai_chat_stream_chunks (id) VALUES (?)'], | ||
| ['cf_mcp_ prefix', 'SELECT * FROM cf_mcp_agent_event'], | ||
| ['schema version', 'SELECT version FROM cf_schema_version'], | ||
| ])('returns true for %s on internal tables', (_label, query) => { | ||
| expect(targetsCloudflareInternalTable(summarize(query))).toBe(true); | ||
| }); | ||
|
|
||
| it('returns true for an internal JOIN', () => { | ||
| const query = ` | ||
| SELECT f.fiber_id, f.status | ||
| FROM cf_agents_fibers f | ||
| LEFT JOIN cf_agents_runs r ON r.id = f.fiber_id | ||
| WHERE f.status IN ('pending', 'running') | ||
| `; | ||
| expect(targetsCloudflareInternalTable(summarize(query))).toBe(true); | ||
| }); | ||
|
|
||
| it('returns true when an internal table is joined with a user table', () => { | ||
| // `.some()` — any internal table present means the query is framework-driven noise. | ||
| expect( | ||
| targetsCloudflareInternalTable(summarize('SELECT * FROM cf_agents_state s JOIN users u ON u.id = s.id')), | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| it('handles case-insensitive keywords and prefixes', () => { | ||
| expect(targetsCloudflareInternalTable(summarize('select * from CF_AGENTS_STATE'))).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe('user queries (must be instrumented)', () => { | ||
| it.each([ | ||
| ['SELECT', 'SELECT * FROM users WHERE id = ?'], | ||
| ['INSERT', 'INSERT INTO orders (id, total) VALUES (?, ?)'], | ||
| ['UPDATE', 'UPDATE products SET price = ? WHERE id = ?'], | ||
| ['DELETE', 'DELETE FROM sessions WHERE expired = 1'], | ||
| ['CREATE TABLE', 'CREATE TABLE users (id TEXT PRIMARY KEY)'], | ||
| ['table with cf in the middle', 'SELECT * FROM my_cf_table'], | ||
| ['table starting with cfg', 'SELECT * FROM cfg_settings'], | ||
| ])('returns false for %s on user tables', (_label, query) => { | ||
| expect(targetsCloudflareInternalTable(summarize(query))).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('allowlist (opt a cf_ table back into instrumentation)', () => { | ||
| it('returns false for an allowlisted table matched by exact string', () => { | ||
| expect(targetsCloudflareInternalTable(summarize('SELECT * FROM cf_my_table'), ['cf_my_table'])).toBe(false); | ||
| }); | ||
|
|
||
| it('returns false for an allowlisted table matched by regex', () => { | ||
| expect(targetsCloudflareInternalTable(summarize('SELECT * FROM cf_reports_daily'), [/^cf_reports_/])).toBe(false); | ||
| }); | ||
|
|
||
| it('requires an exact match for string entries', () => { | ||
| // Substring matches must not opt a table back in, otherwise `cf_` would allowlist everything. | ||
| expect(targetsCloudflareInternalTable(summarize('SELECT * FROM cf_agents_state'), ['cf_agents'])).toBe(true); | ||
| }); | ||
|
|
||
| it('still skips genuine internal tables that are not allowlisted', () => { | ||
| expect(targetsCloudflareInternalTable(summarize('SELECT * FROM cf_agents_state'), ['cf_my_table'])).toBe(true); | ||
| }); | ||
|
|
||
| it('still skips when an internal table is joined with an allowlisted table', () => { | ||
| expect( | ||
| targetsCloudflareInternalTable(summarize('SELECT * FROM cf_my_table t JOIN cf_agents_state s ON s.id = t.id'), [ | ||
| 'cf_my_table', | ||
| ]), | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| it('ignores an empty allowlist', () => { | ||
| expect(targetsCloudflareInternalTable(summarize('SELECT * FROM cf_agents_state'), [])).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe('summaries without a resolvable table target (safe default: instrument)', () => { | ||
| it.each([ | ||
| ['undefined', undefined], | ||
| ['empty', ''], | ||
| ['no-table SELECT', 'SELECT 1'], | ||
| ['PRAGMA', 'PRAGMA foreign_keys = ON'], | ||
| ['bare operation', 'BEGIN'], | ||
| ])('returns false for %s', (_label, value) => { | ||
| const summary = typeof value === 'string' ? summarize(value) : value; | ||
| expect(targetsCloudflareInternalTable(summary)).toBe(false); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.