Skip to content

[4.0] capture* with explicit Scope #1061

@HazAT

Description

@HazAT

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 :)

Metadata

Metadata

Assignees

Labels

No labels
No labels
No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions