Follow up: #1051
From our internal discussion:
Explicit Scope aka. second capture argument/kwargs
Approver: Daniel Griesser
Contributors: Kamil Ogórek, Armin Ronacher
Created: May 15, 2020 3:04 PM
Created by: Daniel Griesser
Decision Date: May 15, 2020
Driver: Daniel Griesser
Informed: Markus Unterwaditzer, Árpád Borsos, Rodolfo Carvalho, Katie Byers, Bruno Garcia, Manoel Aranda, Philipp Hofmann
Status: Completed
Decision: Option A
Note: We will implement and add this to SDKs on a per need basis. Meaning the next time we touch it we should implement this.
tl;dr
// The global scope state will never be mutated
// the second arg is used as a replacement for `withScope`
captureException(new Error("test"), (scope) => {
// Used with a callback clones the current scope and mutates the scope
});
// Used with kwargs clones the current scope and mutates the scope
captureException(new Error("test"), {level: "debug", tags={a: "b"}});
// Used with Scope instance
const scope = new Scope();
scope.setTags({a: "b"});
captureException(new Error("test"), scope);
Goals:
We want to enable one-off scope modification for capture calls without having to configure/push scope.
Motivations:
- Users find the current API is too complex for single cases
- If a hub is shared between threads then push/pop scope is unsafe. Sharing hubs is already permitted between threads.
Github References:
https://github.com/getsentry/sentry-javascript/issues/1607
Option A: Explicit Scopes with Well Known Data
This proposal proposes to let users instantiate and modify scopes and pass these to the global and per-hub capture methods.
Capture methods can also take well-known data keyword arguments if the language supports that and create an implicit scope. This can also be a plain old data object in javascript.
For instance user={} sets the user attribute on a new scope.
Example
# Make a new scope and pass it to a single capture. In this example an
# attachment is added to the scope
scope = Scope()
scope.add_attachment(...)
capture_exception(Error(...), scope=scope)
# Here we use the scope in a way that sets the user for a single call.
scope = Scope()
scope.set_user({'id': 42})
capture_message("Whatever", scope=scope)
# Additionally `user` is well-known data and can be set directly. This is
# equivalent to the call above.
capture_message("Whatever", user={"id": 42})
# for SDKs with callbacks alternative usage:
def callback(scope):
scope.set_user({"id": 42})
capture_message("Whatever", scope=callback)
Well Known Data
user (as dict/object)
level (as string)
extra (as dict)
contexts (as dict)
tags (as dict)
fingerprint (as string array)
Attributes we likely want to add to the scope:
release (? add to scope)
dist (? add to scope)
environment (as string)
Questions
Q: Does the passed scope and the hub scope apply?
A: Yes, the current global scope should always be merged with the arguments passed.
Implementation
Python
def capture_event(self, event, scope=None, **kwargs):
client, top_scope = self.stack[-1]
if client is None:
return
final_scope = top_scope
if scope and kwargs:
raise TypeError("can only have kwargs or scope")
if scope is not None:
final_scope = final_scope.clone()
if callable(scope):
scope(final_scope)
else:
final_scope.update_from_scope(scope)
elif kwargs:
final_scope = final_scope.clone()
final_scope.update_from_kwargs(kwargs)
return client.capture_event(event, final_scope)
# usage:
def scope_func(scope):
scope.set_user({"id": 42})
scope.add_attachment(...)
capture_exception(Error("test"), scope_func)
# this doesn't do attachments because it's not a common attribute
capture_exception(Error("test"), user={"id": 42})
scope = Scope()
scope.set_user({"id": 42})
scope.add_attachment(...)
capture_exception(new Error("test"), scope);
Draft implementation: getsentry/sentry-python#633
JavaScript
function captureEvent(event, scope) {
const client = this.getClient();
const topScope = this.getScope();
if (!client) {
return;
}
let finalScope = topScope;
if (scope) {
finalScope = finalScope.clone();
if (scope instanceof Scope) {
finalScope.updateFromScope(scope);
} else if (typeof scope === 'function') {
scope(finalScope);
} else (scope) {
finalScope.updateFromKwargs(scope);
}
}
return client.captureEvent(event, finalScope);
}
// usage:
captureException(new Error("test"), (scope) => {
scope.setUser({"id": 42});
scope.addAttachment(...);
});
captureException(new Error("test"), {
user: {"id": 42}
});
const scope = new Scope();
scope.setUser({"id": 42});
scope.addAttachment(...);
captureException(new Error("test"), scope);
Objective-C
// this gets passed the clone of the scope already
[SentrySdk captureException:err withScopeBlock:^(scope) {
[scope setUser:...];
[scope addAttachment:...];
}];
// this merges the scope
id scope = [[Scope alloc] init];
[scope setUser:...];
[scope addAttachment:...];
[SentrySdk captureException:err withScope:scope];
Java
// Capture an exception while being able to mutate the final scope applied to the event.
SentryId captureException(Throwable throwable, ScopeCallback<Scope> finalScopeCallback)
// Capture exception providing an additional scope that gets merged to the current one.
SentryId captureException(Throwable throwable, Scope scopeToMerge);
public interface ScopeCallback {
void execute(Scope scope);
}
// Captures exception while controlling the exact scope applied to the event
hub.captureException(throwable, s -> {
s.removeTag("key");
s.addAttachment("./logs.txt");
});
class Hub {
SentryId captureEvent(SentryEvent event, ScopeCallback<Scope> finalScopeCallback) {
Scope currentScopeClone = this.scopeStack.peek().clone();
finalScopeCallback.execute(currentScopeClone);
this.sentryClient.captureEvent(event, currentScopeClone);
}
SentryId captureEvent(SentryEvent event, Scope scope) {
this.sentryClient.captureEvent(event, scope);
}
SentryId captureEvent(SentryEvent event) {
Scope currentScope = this.scopeStack.peek();
this.sentryClient.captureEvent(event, currentScope);
}
}
class SentryClient {
SentryId captureEvent(SentryEvent event, Scope scope) {
scope.Apply(event);
this.transport.captureEvent(event);
}
}
C#
SentryId CaptureException(Exception exception, Action<Scope> finalScopeCallback);
SentryId CaptureException(Exception exception, Scope scopeToMerge);
hub.CaptureException(ex, e => e.AddAttachment("./logs.txt"));
// Copy Java and make PascalCase :)
Follow up: #1051
From our internal discussion:
Explicit Scope aka. second capture argument/kwargs
Approver: Daniel Griesser
Contributors: Kamil Ogórek, Armin Ronacher
Created: May 15, 2020 3:04 PM
Created by: Daniel Griesser
Decision Date: May 15, 2020
Driver: Daniel Griesser
Informed: Markus Unterwaditzer, Árpád Borsos, Rodolfo Carvalho, Katie Byers, Bruno Garcia, Manoel Aranda, Philipp Hofmann
Status: Completed
Decision: Option A
Note: We will implement and add this to SDKs on a per need basis. Meaning the next time we touch it we should implement this.
tl;dr
Goals:
We want to enable one-off scope modification for capture calls without having to configure/push scope.
Motivations:
Github References:
https://github.com/getsentry/sentry-javascript/issues/1607
Option A: Explicit Scopes with Well Known Data
This proposal proposes to let users instantiate and modify scopes and pass these to the global and per-hub capture methods.
Capture methods can also take well-known data keyword arguments if the language supports that and create an implicit scope. This can also be a plain old data object in javascript.
For instance
user={}sets the user attribute on a new scope.Example
Well Known Data
user(as dict/object)level(as string)extra(as dict)contexts(as dict)tags(as dict)fingerprint(as string array)Attributes we likely want to add to the scope:
release(? add to scope)dist(? add to scope)environment(as string)Questions
Q: Does the passed scope and the hub scope apply?
A: Yes, the current global scope should always be merged with the arguments passed.
Implementation
Python
Draft implementation: getsentry/sentry-python#633
JavaScript
Objective-C
Java
C#