-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
feat(node-experimental): Update to new Scope APIs #9799
Conversation
|
||
// Update the isolation scope, isolation this request | ||
if (getSpanKind(span) === SpanKind.SERVER) { | ||
setIsolationScope(Scope.clone(getIsolationScope())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the actual code for auto-isolation with OTEL. Should work for any instrumentation that builds on top of http, so e.g. both express and fastify etc. are covered by this. (in theory, more testing will be needed)
f6ba1cd
to
c25d4e8
Compare
size-limit report 📦
|
* Fork a scope from the current scope, and make it the current scope in the given callback. | ||
* Additionally, also setup a new isolation scope for the callback. | ||
*/ | ||
export function withIsolationScope<T>(callback: (scope: Scope, isolationScope: Scope) => T): T { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't fully understand the callback type here.
- What scope is the
scope
argument? - Why do we need the
scope
argument?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So we have withScope((scope) => {})
, which I think is clear - you get the scope as an argument.
Now withIsolationScope()
is withScope()
that also forks the isolation scope. So my idea was to pass both the forked current scope, as well as the forked isolation scope to the callback 🤔 would it be clearer to name them callback(currentScope: Scope, isolationScope: Scope)
? Or we can also leave this away and only pass the isolation scope, and expect users to do getCurrentScope()
if they need to inside of the callback 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm. Just thinking about things naively I find it odd that withIsolationScope would create more than just an additional scope that happens to be an isolation scope. The way I initially read this function would have been that scope
always is the same as isolationScope
. Is that not the case? I would expect the currentScope
inside withIsolationScope
to be the isolation scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, the isolation scope is never a current scope - like the global scope, it is completely separate. otherwise, inheritance of scopes would be messed up - think this:
withIsolationScope(scope => {
// scope is both current & isolation scope
scope.setTag('xx', 'yy');
withScope(innerScope => {
// innerScope inherits from the previous current scope,
// which would now be the isolation scope, which is bad
});
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it bad to fork from the isolation scope?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's bad if that happens implicitly, without the user knowing this. Keep in mind that in 99% of cases users will never call withIsolationScope
manually but the isolation scope will be set under the hood, and users should not accidentally mutate it by accessing getCurrentScope()
and putting something on it.
I've updated the function syntax to only pass the isolation scope for withIsolationScope
.
You can refer to this graphic which hopefully explains what inherits from what where: https://github.com/getsentry/rfcs/blob/c4b1fb4fd2915246760156285393340af093d591/text/0122-images/scope-inheritance.png
newScope._tags = { ...this['_tags'] }; | ||
newScope._extra = { ...this['_extra'] }; | ||
newScope._contexts = { ...this['_contexts'] }; | ||
newScope._user = this['_user']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should probably also shallow clone user (?)
runWithAsyncContext<T>(callback: () => T): T; | ||
} | ||
|
||
export interface SentryCarrier { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we hard-depending on this anywhere? Idealy we get rid of this in v8.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In how far? We still need some way to keep the global stuff, or how would you keep that instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use a const that is exported instead of a global variable. That way we also won't have collisions if there are multiple Sentry versions on the same browser page for some reasons i believe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense I think, let's see if I can somewhat easily update this in here already!
export type ExclusiveEventHintOrCaptureContext = | ||
| (CaptureContext & Partial<{ [key in keyof EventHint]: never }>) | ||
| (EventHint & Partial<{ [key in keyof ScopeContext]: never }>); | ||
|
||
/** | ||
* Parse either an `EventHint` directly, or convert a `CaptureContext` to an `EventHint`. | ||
* This is used to allow to update method signatures that used to accept a `CaptureContext` but should now accept an `EventHint`. | ||
*/ | ||
export function parseEventHintOrCaptureContext( | ||
hint: ExclusiveEventHintOrCaptureContext | undefined, | ||
): EventHint | undefined { | ||
if (!hint) { | ||
return undefined; | ||
} | ||
|
||
// If you pass a Scope or `() => Scope` as CaptureContext, we just return this as captureContext | ||
if (hintIsScopeOrFunction(hint)) { | ||
return { captureContext: hint }; | ||
} | ||
|
||
if (hintIsScopeContext(hint)) { | ||
return { | ||
captureContext: hint, | ||
}; | ||
} | ||
|
||
return hint; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is messed up but I don't know what to do about it. Is captureException api just garbage?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this is because in the global Sentry.captureException()
the API used to be captureException(exception, captureContext)
, while in other places the second argument is EventHint
.
We could deprecate this for v8 and require this to be a hint, which means that users need to refactor:
Sentry.captureException(error, { captureContext: { tags: ... } })
🤷
/** | ||
* Fork a scope from the current scope, and make it the current scope in the given callback. | ||
* Additionally, also setup a new isolation scope for the callback. | ||
*/ | ||
export function withIsolationScope<T>(callback: (scope: Scope, isolationScope: Scope) => T): T { | ||
const ctx = context.active(); | ||
const currentScopes = getScopesFromContext(ctx); | ||
const scopes = currentScopes | ||
? { ...currentScopes } | ||
: { | ||
scope: getCurrentScope(), | ||
isolationScope: getIsolationScope(), | ||
}; | ||
|
||
scopes.isolationScope = scopes.isolationScope.clone(); | ||
|
||
return context.with(setScopesOnContext(ctx, scopes), () => { | ||
return callback(getCurrentScope(), getIsolationScope()); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q: When would we use this function? Asking specifically because I'm not sure if cloning the isolation scope makes sense. Shouldn't a new isolation scope already be fresh and not potentially carry over data from another execution context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We decided to fork the isolation scope, because otherwise you may loose data. Think of this realistic scenario:
Sentry.init();
Sentry.setTag('xx'); // <-- this actually writes on the initial isolation scope
app.get('/my-route', function() {
// this has a forked isolation scope - but it has inherited the xx tag from outside!
// so the tag is still present on stuff in here
}
Does that make sense?
For when to use it, this should mostly be used by ourselves in auto instrumentation, or if users want to do that themselves. Alternatively you can also use setIsolationScope()
, which overwrites the isolation scope for the current execution context - whatever works better for the concrete use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I guess that makes sense. I was under the impression that Sentry.setTag('xx')
would write to the global scope and that we'd only create the first isolation scope when a request hits /my-route
. Hence my question. But if we create one beforehand, then it makes sense to fork.
Anyway, my main concern here is avoiding bleed of e.g. user data between isolation scopes which is why I had mixed feelings about forking it.
/** | ||
* This is for legacy reasons, and returns a proxy object instead of a hub to be used. | ||
*/ | ||
export function getCurrentHub(): Hub { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we deprecate this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good question! I think for node-experimental it doesn't matter, but it's a general thing to think about for v8. I tend to say "yes"!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean the whole idea of the scope changes is to get rid of getCurrentHub
, so I agree 😅
d57996f
to
9730647
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for answering my questions!
Co-authored-by: Luca Forstner <luca.forstner@sentry.io>
45272eb
to
13c7856
Compare
This is a try to move the application of scope data to events out of the scope class into a util function. This changes the flow to be: 1. The scope has a `getScopeData()` method that returns data that should be applied to events. 2. The `prepareEvent` method uses that to actually apply scope data to the event etc. 3. This also makes it much easier to do the experimentation in node-experimental (#9799) because we only need to overwrite this, and can leave the rest of the event processing the same. I _think_ this makes sense but would appreciate some more eyes on this as well. For me the separation makes also more sense, as e.g. the application of event processors etc. should not really be tied to the scope at all. This is also is a first step to then allow us to add global scopes more easily. --------- Co-authored-by: Lukas Stracke <lukas.stracke@sentry.io>
This PR introduces the new scope APIs to node-experimental.
getCurrentHub()
is still around, but just a mock hub that uses other methods under the hood.getCurrentScope()
getIsolationScope()
getGlobalScope()
withIsolationScope()
Mostly existing tests should cover this OK. The main change here is that for spans, since we use the isolation scope any tags etc. added while the span is running are also added to the resulting event.
For POTEL, we automatically set an isolation scope whenever a http.server span is generated.
Replaces #9419