Skip to content

Handle async routes in the Mojolicious plugin #28

@rabbiveesh

Description

@rabbiveesh

Because the sentry integrations are global, they don't play nicely out of the box with async; in my project at work i've written my own sentry plugin around this one that uses Syntax::Keyword::Dynamically and Future::AsyncAwait to solve this, relevant bits below -

  $app->hook(
    around_action => sub ($next, $c, $action, $last) {
      my $req = $c->req;

      my $scope       = $c->stash('_sentry_scope');
      my $transaction = $c->stash('_sentry_transaction');
      unless ($scope) {

        # eliminate any non-request scopes, b/c memory leaks for the lose
        Sentry::Hub->get_current_hub->reset;
        Sentry::SDK->configure_scope(sub ($new_scope) {
          my $transaction = Sentry::SDK->start_transaction({
            name    => $c->match->endpoint->to_string // '/',
            op      => 'http.server',
            request => {
              url     => $req->url->to_abs->to_string,
              method  => $req->method,
              query   => $req->url->query->to_hash,
              headers => { pairgrep { $a ne 'Cookie' } $req->headers->to_hash->%* }
            },
            $self->continue_from_headers($req->headers)->%*
          });
          $new_scope->set_span($transaction);

          $c->stash(_sentry_scope       => $scope = $new_scope);
          $c->stash(_sentry_transaction => $transaction);
        });
      }

      # save our scope for this dynamic context
      dynamically Sentry::Hub->get_current_hub->{scopes} = [$scope];

      # turn on breadcrumbs for this dynamic context
      dynamically Sentry::Integration->get_integration('DBI')->{breadcrumbs}           = 1;
      dynamically Sentry::Integration->get_integration('MojoUserAgent')->{breadcrumbs} = 1;

      return $next->() if $c->tx->is_websocket;

      # always returns a promise
      $self->handle_action($c, $next, $last);

      # break the dispatch chain, but the action handler may actually continue it
      return $last;
    }
  );

and the call to handle_action is

async sub handle_action ($self, $c, $next_hook, $last) {
  # we must hold onto the $tx here, b/c the req may get cancelled while await-ing
  my $tx;
  try {
    $tx = $c->render_later->tx;
    my $fin = $next_hook->();
    $fin = await $fin if blessed $fin and $fin->can('AWAIT_IS_READY');

    # we need to delay to avoid an infinite loop
    Mojo::IOLoop->next_tick(sub {
      $c->continue if $fin && !$last;
    });
  } catch ($err) {
    $c->reply->exception($err);
    Sentry::SDK->capture_exception($err) if $c->res->is_server_error;
  } finally {
    if ($last) {
      my $transaction = $c->stash('_sentry_transaction');
      if (defined $c->stash('sentry_tracing')) {
        $transaction->sampled($c->stash('sentry_tracing'));
      }
      $transaction->set_http_status($c->res->code) if $c->res->code;
      $transaction->finish();
    }
  }
}

Would be happy to open a PR, just wanna know if the dependencies are acceptable for the scope of this project.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions