Skip to content

rfc(feature): Capture SDK options [WIP] - #162

Draft
mydea wants to merge 8 commits into
mainfrom
rfc/capture-sdk-options
Draft

mydea wants to merge 8 commits into
mainfrom
rfc/capture-sdk-options

Conversation

@mydea

@mydea mydea commented Sep 21, 2026

Copy link
Copy Markdown
Member

WIP — draft RFC, still in progress.

Proposes a mechanism for SDKs to report the configuration they were initialized with (the arguments passed to Sentry.init(), plus relevant derived/effective values) to Sentry, so it can be stored, surfaced, and acted upon.

Linear project: https://linear.app/getsentry/project/capture-sdk-options-js-08e8a89c71c9/overview

Rendered RFC

🤖 Generated with Claude Code

mydea and others added 8 commits September 21, 2026 13:50
Add initial boilerplate draft for capturing Sentry SDK init options.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Describe that we do not capture SDK config today, and the adjacent
signals (event SDK metadata, client reports). Remove the empty
Supporting Data section for now.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Option A (preferred): a dedicated, first-class envelope item for SDK
options. Option B: expand error events to carry the config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Propose a language-agnostic, primitives-only payload: sdk (identity +
opt-in integration status via `active`), meta, options, integration_options,
and a free-form `_other` bucket. Cover callback normalization, generic
integration options, reflecting which integrations are actually used, and
cross-SDK naming.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Sending: server SDKs (debounced send after init, configurable delay,
  flush on shutdown), client SDKs (send-on-every-init vs. sampling),
  and a note that serverless shares client constraints.
- Storing: store-every-record vs. dedup-by-release (recommended), with
  an open question on whether release is a sufficient dedup key.
- Add top-level `timestamp` to the envelope; note options are scrubbed
  server-side (no client-side scrubbing).
- Add Drawbacks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename XXXX -> 0162 now that the PR is open, fill in the RFC PR link,
and add the index entry to README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an "Envelope item type" subsection recommending `sdk_config`, with
`sdk_options` and `client_config` as considered alternatives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add "Not in scope / Follow-up work" (product usage; removing SDK
  metadata from events, which needs a backfill from sdk_config).
- Cross-SDK naming: recommend leaving option names unspecced (native
  SDK names as-is) rather than a canonical catalog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +45 to +53
- Analytics of which options are used, for decision-making about docs, deprecations, and majors.
- Flagging configuration changes that might affect trends over time when querying.
- Auditing `init()` setups to suggest helpful changes or warn about confusing behavior.
- Showing users a list of all the `Sentry.init()`s producing data into each project, so they
can understand their data sources and any filtering/sampling/config that affects them.
- Showing changes to configuration over time.
- A UI for turning sources on and off and making configuration changes (e.g. via Seer PRs).
- A clear discoverability element — showing which data sources are configured to produce (and
not produce) which telemetry types.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reads repetitive to the content above

Comment on lines +201 to +211
"options": {
"dsn": "https://<public-key>@o0.ingest.sentry.io/0",
"sampleRate": 1.0,
"tracesSampleRate": 0.2,
"sendDefaultPii": true,
"debug": false,
"beforeSend": "[Function]",
"tracesSampler": "[Function]",
"denyUrls": ["https://example.com/ignore"],
"integrations": ["InboundFilters", "MyIntegration"]
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these not be objects, so we can also track who/what set the respective options? (default value vs. user defined but potentially matching with the default etc).

So a config like

Sentry.init({
  dsn: "foo",
  tracesSampleRate: 0.2
});

would lead to something like

    # implied config
    "sampleRate": {
        "value": 1.0,
        "default": 1.0,
        "init": false
    },
    # explicit config
    "tracesSampleRate": {
        "value": 0.2,
        "default": 1.0,
        "init": true
    }

naming tbd

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or would the idea be to have the defaults in integration_options?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought

"init": false

should probably be

"source": "init"    # or "default", "env-var"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should all only be user-defined stuff. defaults should not be reflected here - but this should likely be actually explained in the text :D

@dingsdax dingsdax Sep 22, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaults should not be reflected here

I'm torn about this. Do we want to answer “what did the user explicitly configure?” or “what configuration is this SDK running with?” user overrides are enough for the former. For debugging/setup audits, resolved defaults matter too, and reconstructing them server-side requires SDK/version-specific resolution logic: Ruby and Python already merge defaults with user configuration for instance.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my POV, most things I can think about that we'd want to do could be solved via user-provided options only IMHO. And it would likely drastically reduce complexity (and likely payload size) - way easier to send the user provided options than to have a good hook that ensures all defaults are properly set etc, while also maintaining a way to know which are defaults and which are not. I would consider implementation complexity as a relevant factor here - IMHO we should have a good case for why we want to capture defaults. If we decide we want to capture defaults as well, I think I'd rather opt to capture them separately (e.g, default_options) instead of requiring us to merge this somehow into a specific object shape at runtime.

To show the challenge, this is how this is most of the time handled today:

export function init(options: BrowserOptions): Client | undefined {
  const opts = {
   // add defaults, transform stuff, etc.
    ...options,
  };

  applySdkMetadata(opts, 'react');
  const client = browserInit(opts);

  return client;
}

If we capture only user provided options, it is basically captureSdkOptions(options). If we need to consider defaults, we need to somehow pass it both the final options, and have some logic that reliably diffs the passed in options from the final options, which is possible of course but considerably more complicated likely. Not saying that blocks this, but just as additional context :)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

> I think I'd rather opt to capture them separately (e.g, default_options)

yes that was my point, if we do that, let's separate, but I can see that being valuable and easier than somehow reconstructing on the server side what could have been the default at that point of time

Comment on lines +295 to +303
**Recommendation: leave the option names unspecced.** By design, the payload uses each SDK's
**native option names as-is** — SDKs simply report options as they are named in that SDK, with
no normalization to a shared vocabulary. This makes cross-SDK analysis harder (the server, or a
consumer, has to reconcile differently-named-but-equivalent options), but it is far easier to
reason about and implement in the SDKs: there is nothing to map, nothing to keep in sync, and
new options are captured automatically without a catalog change. Given the RFC starts with JS
and the primary near-term value is per-SDK/per-release insight, this trade-off is worth it; a
canonical mapping can be layered on later (server-side or in analysis) if cross-SDK comparison
becomes important.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree, I don't think every SDK should send their own config option names, but we should instead focus on a protocol that each SDK implements against. Even if it means they need to map tracesSampleRate to traces_sample_rate or the other way around.

This is especially important since not all SDKs even have the same configuration structure, e.g. where and how integrations get enabled. So at one point we need to align, and I'd rather have that on the protocol level than forcing the server to know about all constellations.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but a lot of options will not exist in other sdks. so you'll have to maintain a map of fields to map in each sdk, which is complexity and bundle size in SDK. imho it would be nicer to handle this on the sentry side. Also, you'll get a mix-and-match of unified and non-unified fields. (I would guess a good amount of options are non-standard between sdks).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some additional thoughts on this: standardizing this sdk-side is both considerable overhead (both in complexity and in size), and also has the problem that it is not really understandable for users later. e.g. let's say we show a user what sample rate they configured, would we show a js user "you configured trace_sample_rate: 0.1"? this is much easier if we send this in the way that the user also set it.

If we need this, I'd rather have us do this in relay. We could define the options that are relevant for us and e.g. populate a standardized_options object in relay based on the options into a format that we want. This way, we'd have access to both the values as the user set it, as well as to a standardized format where we need it.

In regard to where and how config is set - this should not be relevant for the format, no matter where exactly this is set, it should all fold into this option. So e.g. if a user does:

Sentry.init({
  dsn: 'x'
});
Sentry.addIntegration(Sentry.customIntegration());

it should result in the same object being sent as:

Sentry.init({
  dsn: 'x',
  integrations: [Sentry.customIntegration()]
})

for simplicity and consistency sake.

### Option 2 (recommended): Deduplicate and store a single record

Store only **one** record per configuration and discard the rest. Because configuration is
essentially constant per release, we need a key to deduplicate by, and we suggest **release**:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we go for release we could still end up with many different init constellations because of options passed in by ENV vars

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this

_within_ the same release (e.g. environment-dependent options, feature flags, per-deployment
overrides, or code paths that call `init()` with different options), and release may be unset
in some setups. We may need a composite key (e.g. release + environment, or a hash of the
normalized options) or a different identifier altogether. This needs to be validated before

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a hash of the normalized options

I like this one the most. We could even attach this hash with to other payloads we send out (e.g. events, transaction, so we can ultimately achieve the connect that Dave was envisioning).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could do this, it also comes down a bit to product stuff as well - if we dedupe it by release (or similar), we can also leverage this restriction in product much easier. e.g. "This config was introduced in release X". If we dedupe it by options hash, and there can be multiple per release, this becomes much harder to handle in UI too (e.g. This config was introduced in some instance of release X??").

My POV is that I would imagine that a per-release or similar (maybe a combination of keys, e.g. release+env or similar) deduping is probably good enough for 98% of cases of so and allows us to make some easier to handle assumptions in product etc. Also likely it is less overhead at ingest, but this is probably not too important I imagine. We can then also easily attach each event to some config based on the release.

So my concern is, doing this on a hash basis is technically correct and covers all cases, but also leads us to have to handle the most-complicated edge cases everywhere all the time then. Not sure if this is worth it 🤔

mirror the user's input, with all values reduced to primitives per the rules above. The
`integrations` init option is represented here as a list of names; the details live in the
dedicated `integration_options` block to avoid duplicating (and bloating) the raw options.
Fields in `options` should be **scrubbed server-side** for sensitive data (especially tokens

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why only server-side?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplicity? I'd like to keep this as low-overhead in sdks as possible. There should not be too much really sensitive in there IMHO (can't think of many sensitive things in sdk config from the top of my head?), if we know of things that are always sensitive we can of course omit them sdk side, but could not come up with anything so far.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, would need to look at each SDK separately, but I could images we might do some weird merging shit in some SDKs, where sensitive information shows up in the config all of a sudden at the point of export 🙂

For server SDKs, the payload is sent **once, shortly after `init()`**, guarded by a short
**debounce delay**:

- **Debounced send after init.** Rather than sending synchronously at the end of `init()`, the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could SDK/framework lifecycle hooks be preferred where available, with debounce as a fallback?
For instance Rails modifies configuration and activates instrumentation in after_initialize, after Sentry.init; a fixed delay can fire before that finishes.
How about "Let each SDK choose an appropriate readiness point and include pending reports in existing flush/shutdown handling"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, def. I can reword this to be more explicit on this! debounce is the most primitive form. the key should be that we consider stuff that happens right after init too and not just send this inside of the init() function or whatever.

This section proposes the shape of the dedicated SDK-options payload (question **a** above).
The goal is a **language-agnostic** shape that works equally for JavaScript, Python, and every other SDK, so the server can handle a single, consistent schema.

## Envelope item type

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just mentioning it here:

a new item type might need: rate-limit category, payload limits, and compatibility behavior or decisions for older Relay/self-hosted versions? e.g. Ruby and Python currently classify unknown item types as default, so these semantics need an explicit decision. Would add something like "Capturing SDK options should honor SDK sending restrictions, use existing transport/flush mechanisms, and fail without affecting application behavior." for completeness

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, we need to consider how this works/degrades on self-hosted versions that do not support this too.

map keyed by integration name, where the value is a small, **opt-in** status object:

- The **presence of a key** means the integration is registered/enabled.
- An optional **`active`** boolean means the integration determined at runtime whether it is

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get "enableLogs" PTSD here :).

Could we separate successful instrumentation from observed usage rather than call both “active”? We could derive usage server-side where telemetry identifies the integration, recording last_seen and evaluating it over a defined window. Maybe I'm oversimplifying here if that is possible at all. Absence would mean “not observed,” not inactive. That would leave SDK reporting focused on registration and, optionally, whether instrumentation was successfully installed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm not 100% sure what you mean? If there is a signal already captured to infer usage, we can already solve this today. The problem we have is that we don't necessarily have such a signal today in JS. We do know if a certain package was instrumented at all, and I'd like to include this information somehow. IMHO "this library was instrumented" should be safe to rely on as a data point.

maybe "active" is not a good wording here 🤔 we can call it "is_instrumenting" or something along these lines too?

I would prefer to keep this atomic in "this is sent and never updated" vs. having some background tasks that have to update the config as things come in, that defeats the idea of this being simple and low-overhead 😅

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, read it again and look at the snippet, I somehow mixed that up with a top-level option, all good, I understand what you mean here.

per page load / app launch / session, at client-traffic volumes), and it adds a request for
users on every init.

### Option II: Sampling

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels like a non-option to me:
missing data must mean “not observed,” not “disabled” or “unused,” particularly if we want to inform setup audits or deprecation decisions for instance.

intuition is that configuration is essentially constant per release
I don't think this is true. Configuration can change independently of releases: the same release may run as web and background-worker processes with different settings, or be restarted with updated ENV vars. variables.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, release alone is likely not good enough. I wonder/hope we could come up with a combination of keys that we can agree on define a unified config. Then there may obviously be some edge cases that still don't follow this but if we get to a 99% case of coverage, I think it would help us streamline assumptions, UI, etc. around this.

E.g.

  • release + environment

What are common cases we can think of where init option would vary above that? 🤔 When would you restart the same process with different env vars (that are relevant for sentry?) Commonly you look at environment, of course. (And not saying it can never happen, but does it happen often enough and even if it happens, how problematic is it if we miss that here or there)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but then again, this is probably an edge case anyway

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants