## Summary After the 2026-04-26 token-usage incident, identifying the responsible workspace from a Sentry trace required a Postgres scavenger hunt — Vercel AI SDK auto-instrumentation captures token counts and model name but no twenty-specific identifiers, and that same gap exists for every other auto-instrumented span (HTTP outbound, Postgres queries, GraphQL resolvers, Redis, etc.). This PR plugs that gap globally, not just for AI: - A small utility (`packages/twenty-server/src/engine/core-modules/sentry/utils/sentry-workspace-context.util.ts`) that writes workspace identifiers onto Sentry's active isolation scope as a `twenty` context block plus filterable tags and a `Sentry.setUser` call. - Two hook points covering all server traffic: - **`WorkspaceAuthContextMiddleware`** — already runs after token hydration on the GraphQL, metadata, admin-panel, and REST routes. It now calls the utility once per authenticated request, before delegating to `withWorkspaceAuthContext`. - **`BullMQDriver.work` and `SyncDriver.processJob`** — every queue job now runs inside `Sentry.withIsolationScope` and applies workspace context from `job.data.workspaceId` (skipping silently for system jobs that don't carry one). - A `beforeSendSpan` hook in `instrument.ts` that reads the scope's `twenty` context block back and projects it onto every span as `twenty.workspace.id` and (when available) `twenty.user_workspace.id` — dotted-namespace naming consistent with OTel/Sentry conventions like `user.id` and `http.response.status_code`. Spans without a workspace context (unauthenticated traffic) pass through untouched. ## Why this shape Sentry's docs position `beforeSendSpan` as a per-span hook. The previous iteration set context only at AI-specific call sites, which left non-AI spans (DB queries, outbound HTTP, regular GraphQL queries, workflow steps not touching AI) entirely unenriched. Hooking the two existing global boundaries — auth middleware for HTTP/GraphQL/REST, and the queue driver `work()` callback for background jobs — covers every authenticated span across the app with no per-handler instrumentation. ## What's not in this PR AI-specific identifiers (`twenty.agent.id`, `twenty.thread.id`, `twenty.turn.id`, `twenty.workflow_run.id`) are out of scope here. They're useful additions but require either propagating the IDs through the call stack or a more fine-grained scope (per-step, per-turn) than the request/job boundary, which is best handled in follow-up PRs that target those specific call sites. ## Test plan - [ ] Make any authenticated GraphQL request locally and confirm the resulting span(s) in Sentry carry `twenty.workspace.id` and (for user-authenticated routes) `twenty.user_workspace.id`. - [ ] Make any authenticated REST request and confirm the same. - [ ] Trigger a queue job (chat stream, agent turn evaluation, workflow run, etc.) and confirm spans produced inside the worker carry `twenty.workspace.id`. - [ ] Confirm that DB and outbound HTTP spans produced under the request/job also carry the workspace tag — these previously had no twenty-specific identifiers. - [ ] In the Sentry UI, filter events by the `twenty.workspace.id` tag and confirm matching events appear. ## Notes for review - Sentry init lives in `instrument.ts`, loaded before Nest bootstraps, so `beforeSendSpan` runs outside Nest DI and reads context off the isolation scope rather than holding a service reference. - The middleware change is three lines; the BullMQ wrap is a single `Sentry.withIsolationScope` around the existing job handler body; the SyncDriver wrap mirrors it for the dev/test path. No new modules or DI providers. - The previous iteration's `AiCallContextService` and per-handler `setContext` / `withContext` calls have been removed in favor of these two hooks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The #1 Open-Source CRM
Website ·
Documentation ·
Roadmap ·
Discord ·
Figma
Why Twenty
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
Learn more about why we built Twenty
Installation
Cloud
The fastest way to get started. Sign up at twenty.com and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
Build an app
Scaffold a new app with the Twenty CLI:
npx create-twenty-app my-app
Define objects, fields, and views as code:
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
nameSingular: 'deal',
namePlural: 'deals',
labelSingular: 'Deal',
labelPlural: 'Deals',
fields: [
{ name: 'name', label: 'Name', type: FieldType.TEXT },
{ name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
{ name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
],
});
Then ship it to your workspace:
npx twenty deploy
See the app development guide for objects, views, agents, and logic functions.
Self-hosting
Run Twenty on your own infrastructure with Docker Compose, or contribute locally via the local setup guide.
Everything you need
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
Want to go deeper? Read the User Guide for product walkthroughs, or the
Documentation for developer reference.
|
|
|
|
|
|
Stack
TypeScript
Nx
NestJS, with BullMQ,
PostgreSQL,
Redis
React, with Jotai, Linaria and Lingui
Thanks
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
Join the Community
Star the repo ·
Discord ·
Feature requests ·
Releases ·
X ·
LinkedIn ·
Crowdin ·
Contribute





