From 5ac783003d951cfb437143e8adbe784a19105367 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Sat, 3 Oct 2026 18:53:43 +0200 Subject: [PATCH] feat(cloudflare): Skip spans for the pi_ tables of PiHarness The PiHarness of the agents SDK keeps the state of pi-durable in the SQLite database of the Durable Object, in tables with a pi_ prefix. Each statement became a db.query span, close to 200 for one run, and they buried the agent spans of the run. The tables are now treated like the cf_ tables of the agents framework: their statements start no span, and durableObjectSqlSpanAllowlist brings them back. Co-Authored-By: Claude Opus 5.5 --- packages/cloudflare/src/client.ts | 12 +++++---- .../instrumentations/instrumentSqlStorage.ts | 2 +- .../cloudflare/src/utils/internalSqlQuery.ts | 19 ++++++++------ .../test/instrumentSqlStorage.test.ts | 25 ++++++++++++++++++- .../test/utils/internalSqlQuery.test.ts | 2 ++ 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/packages/cloudflare/src/client.ts b/packages/cloudflare/src/client.ts index 1a968cfe66b2..3b4f591d5e22 100644 --- a/packages/cloudflare/src/client.ts +++ b/packages/cloudflare/src/client.ts @@ -371,12 +371,14 @@ interface BaseCloudflareOptions { rpcTracePropagationBindings?: TracePropagationTargets; /** - * Table names that should stay instrumented even though they match the reserved `cf_` prefix used - * by Durable Object frameworks (`agents`, `partyserver`, ...) for their internal SQLite tables. + * Table names that should stay instrumented even though they match a reserved prefix used by + * Durable Object frameworks for their internal SQLite tables: `cf_` (`agents`, `partyserver`, ...) + * and `pi_` (pi-durable in the `PiHarness` of `agents`). * - * By default, `exec` queries against `cf_`-prefixed tables are treated as framework noise and no - * `db.query` span is created for them. If one of your own tables happens to use this prefix, add it - * here to opt it back into instrumentation. Entries are matched against each table name in the + * By default, `exec` queries against tables with these prefixes are treated as framework noise and + * no `db.query` span is created for them. If one of your own tables happens to use such a prefix, + * add it here to opt it back into instrumentation, or add `/^pi_/` to see the statements of + * pi-durable. Entries are matched against each table name in the * query summary — strings must match exactly, while regular expressions give you prefix/pattern * matching. * diff --git a/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts b/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts index 276cd0be5f8d..c34dab1214ba 100644 --- a/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts +++ b/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts @@ -37,7 +37,7 @@ export function instrumentSqlStorage(sql: SqlStorage): SqlStorage { if (childSpanWillNotBeRecorded() && !mayTargetCloudflareInternalTable(query)) { // This span is never sent, so skip the costly sanitize and summary // steps. We still start the span so it records its dropped span - // outcome. A query that may target a `cf_` table takes the full path, + // outcome. A query that may target a `cf_` or `pi_` table takes the full path, // because an internal query must start no span. return startSpan({ name: 'exec', attributes: SPAN_ATTRIBUTES }, callOriginal); } diff --git a/packages/cloudflare/src/utils/internalSqlQuery.ts b/packages/cloudflare/src/utils/internalSqlQuery.ts index 70d45a5af9d8..4ea558850527 100644 --- a/packages/cloudflare/src/utils/internalSqlQuery.ts +++ b/packages/cloudflare/src/utils/internalSqlQuery.ts @@ -9,8 +9,10 @@ import { stringMatchesSomePattern } from '@sentry/core'; * 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. + * use it. The same goes for `pi_`: the `PiHarness` of `agents` keeps the state of pi-durable in + * tables with that prefix, and one run writes close to 200 statements. In case a user table does + * collide with a 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 @@ -38,14 +40,16 @@ export function targetsCloudflareInternalTable( } /** - * Returns `false` when the raw `query` has no word that starts with `cf_`, so it cannot target a - * Cloudflare internal table. Needs no sanitizing, which makes it cheap enough to run on every query. + * Returns `false` when the raw `query` has no word that starts with `cf_` or `pi_`, so it cannot + * target a Cloudflare internal table. Needs no sanitizing, which makes it cheap enough to run on + * every query. */ export function mayTargetCloudflareInternalTable(query: string): boolean { - return CF_PREFIX_RE.test(query); + return INTERNAL_PREFIX_RE.test(query); } -const CF_PREFIX_RE = /\bcf_/i; +const INTERNAL_TABLE_PREFIXES = ['cf_', 'pi_']; +const INTERNAL_PREFIX_RE = /\b(?:cf|pi)_/i; // `CREATE [UNIQUE] INDEX [IF NOT EXISTS] ON ` — the IF EXISTS shape mirrors DDL_RE // in @sentry/core. @@ -53,7 +57,8 @@ const CREATE_INDEX_TABLE_RE = /^\s*CREATE\s+(?:UNIQUE\s+)?INDEX(?:\s+IF\s+(?:NOT\s+)?EXISTS)?\s+[^\s(,;)]+\s+ON\s+(?
[^\s(,;)]+)/i; function isCloudflareInternalTable(table: string, allowlist?: Array): boolean { - if (!table.toLowerCase().startsWith('cf_')) { + const name = table.toLowerCase(); + if (!INTERNAL_TABLE_PREFIXES.some(prefix => name.startsWith(prefix))) { return false; } diff --git a/packages/cloudflare/test/instrumentSqlStorage.test.ts b/packages/cloudflare/test/instrumentSqlStorage.test.ts index edf590f84a0f..7d0092bfcfed 100644 --- a/packages/cloudflare/test/instrumentSqlStorage.test.ts +++ b/packages/cloudflare/test/instrumentSqlStorage.test.ts @@ -218,6 +218,26 @@ describe('instrumentSqlStorage', () => { }); }); + // The Agents SDK `PiHarness` keeps the state of pi-durable in its own tables, all with the `pi_` + // prefix of its session store. + describe('pi-durable tables (pi_ prefix) are skipped', () => { + it.each([ + ['SELECT', 'SELECT record FROM pi_conversations WHERE id = ?'], + [ + 'upsert', + `INSERT INTO pi_tasks (id, conversation_id, kind, status, record) VALUES (?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET status = excluded.status, record = excluded.record`, + ], + ['INSERT OR IGNORE', 'INSERT OR IGNORE INTO pi_record_ids (id, record_type) VALUES (?, ?)'], + ['UPDATE', 'UPDATE pi_durable_metadata SET next_id = ?, next_seq = ? WHERE singleton = ?'], + ['DELETE', 'DELETE FROM pi_document_revisions WHERE document_id = ?'], + ['CREATE TABLE', 'CREATE TABLE pi_durable_schema (version INTEGER NOT NULL) STRICT'], + ['CREATE INDEX', 'CREATE INDEX pi_tasks_by_status ON pi_tasks (status, id)'], + ])('skips %s', (_label, query) => { + expect(execCreatesSpan(query)).toBe(false); + }); + }); + describe('user queries stay instrumented', () => { it.each([ ['SELECT', 'SELECT * FROM users WHERE id = ?'], @@ -228,6 +248,8 @@ describe('instrumentSqlStorage', () => { ['CREATE INDEX', 'CREATE INDEX idx_name ON users (name)'], ['table with cf in the middle', 'SELECT * FROM my_cf_table'], ['table starting with cfg', 'SELECT * FROM cfg_settings'], + ['table with pi_ in the middle', 'SELECT * FROM api_keys'], + ['table starting with pi', 'SELECT * FROM pipelines'], ['INSERT OR REPLACE', 'INSERT OR REPLACE INTO users (id, name) VALUES (?, ?)'], ['REPLACE INTO', 'REPLACE INTO sessions (id, token) VALUES (?, ?)'], ['UPDATE OR IGNORE', 'UPDATE OR IGNORE products SET price = ? WHERE id = ?'], @@ -241,10 +263,11 @@ describe('instrumentSqlStorage', () => { }); }); - describe('durableObjectSqlSpanAllowlist (opt a cf_ table back into instrumentation)', () => { + describe('durableObjectSqlSpanAllowlist (opt a cf_ or pi_ table back into instrumentation)', () => { it.each([ ['exact string', 'SELECT * FROM cf_my_table', ['cf_my_table']], ['regex', 'SELECT * FROM cf_reports_daily', [/^cf_reports_/]], + ['regex for the pi-durable tables', 'SELECT record FROM pi_conversations WHERE id = ?', [/^pi_/]], ['upsert target', 'INSERT OR REPLACE INTO cf_my_table (id) VALUES (?)', ['cf_my_table']], ['CREATE INDEX target', 'CREATE INDEX idx_mine ON cf_my_table (id)', ['cf_my_table']], ])('instruments an allowlisted table matched by %s', (_label, query, allowlist) => { diff --git a/packages/cloudflare/test/utils/internalSqlQuery.test.ts b/packages/cloudflare/test/utils/internalSqlQuery.test.ts index 0c9d6c641dde..7b15a5d6d737 100644 --- a/packages/cloudflare/test/utils/internalSqlQuery.test.ts +++ b/packages/cloudflare/test/utils/internalSqlQuery.test.ts @@ -31,6 +31,7 @@ describe('mayTargetCloudflareInternalTable', () => { ['a quoted cf_ table', 'SELECT * FROM "cf_agents_state"'], ['a schema-qualified cf_ table', 'SELECT * FROM main.cf_agents_state'], ['a cf_ table in a CREATE INDEX ON clause', 'CREATE INDEX idx_agents_state_id ON cf_agents_state (id)'], + ['a pi_ table', 'SELECT record FROM pi_tasks WHERE id = ?'], ])('returns true for %s', (_label, query) => { expect(mayTargetCloudflareInternalTable(query)).toBe(true); }); @@ -39,6 +40,7 @@ describe('mayTargetCloudflareInternalTable', () => { ['a user table', 'SELECT * FROM users WHERE id = ?'], ['a table with cf in the middle', 'SELECT * FROM my_cf_table'], ['a table starting with cfg', 'SELECT * FROM cfg_settings'], + ['a table with pi_ in the middle', 'SELECT * FROM api_keys'], ])('returns false for %s', (_label, query) => { expect(mayTargetCloudflareInternalTable(query)).toBe(false); });