Conversation
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>
| - 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. |
There was a problem hiding this comment.
Reads repetitive to the content above
| "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"] | ||
| }, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Or would the idea be to have the defaults in integration_options?
There was a problem hiding this comment.
On second thought
"init": false
should probably be
"source": "init" # or "default", "env-var"
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
> 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
| **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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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**: |
There was a problem hiding this comment.
If we go for release we could still end up with many different init constellations because of options passed in by ENV vars
| _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 |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 😅
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
but then again, this is probably an edge case anyway
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