Back to Sentry

Pipelines Utility

src/sentry/pipeline/README.md

26.4.26.8 KB
Original Source

Pipelines Utility

The sentry.pipeline module provides functionality for executing a series of views that maintain arbitrary state across each request and moves the user through the "pipeline".

Some key points to understanding pipelines:

  • A pipeline executes a set of views (that receive the pipeline instance), moving through them by having the view itself call pipeline.next_step.

  • Each executed view may maintain state within the pipeline using the pipeline.bind_state method. This state is associated to the user's session.

  • Pipelines are always subclassed to implement the finish_pipeline method, this method will be called when a pipeline completes.

  • Pipelines are given the set of pipeline views (instances of PipelineView subclasses) via the get_pipeline_views method of the Pipeline Provider object that is associated to a particular provider key.

  • Pipelines are usually constructed and executed by either two view endpoints. One to call the pipelines initialize method, and the next which is called to move through the pipeline

  • Pipelines are bound to a specific session when they are being executed.

Pipeline Providers

Pipelines are given their process implementation details through the pipeline provider, an interface class exists, PipelineProvider, which declares the required methods. You can think of the provider as the implementation of the actual processes that the user is being guided through. The provider specifies the pipeline steps.

A single pipeline may have multiple types of providers for the pipeline which define different flows on a per-provider basis, but all complete a similar type of pipeline process.

A good example of a pipeline with multiple types of providers is the sentry.identity.pipeline module, which makes use of a Pipeline to associate user identities. Sentry has various identity types (GitHub, Slack, Google) each which may use a slightly different process to do identity lookup on the external service, however the end of the process (and what is done in the final finish_pipeline call) all result in an Identity object being created.

Pipeline Provider Model

While not explicitly required, a pipeline supports lookup of a model that is associated to a particular pipeline. This allows the pipeline to automatically lookup the model given to the pipeline upon it's first initialization.

This simply moves the boilerplate of looking up a model from the pipeline views, into it already being available as pipeline.provider_model within any view that has access to the pipeline.

The model is configured by the provider_model_cls pipeline class attribute.

Pipeline Views

Pipeline views are objects that implement the PipelineView interface and are used as part of the list of views that a pipeline executes. The pipeline views receive the executing request object when they are the step being executed, along with the pipeline instance itself.

It's the job of the pipeline view to transition the pipeline to the next step in the pipeline and bind any data that may need to be used later in the pipeline or must be available when the pipeline completes.

An example pipeline view might be a form that asks the user for some input.

python
class GetUserInput(PipelineView):
    def dispatch(self, request: HttpRequest, pipeline: Pipeline) -> HttpResponseBase:
        # The pipeline supports a generic error method that will render a
        # pipeline error view
        if 'my_data' not in request.POST:
            return pipeline.error('Data was not sent')

        # Form submitted, bind user data and move to and render the next step
        # in the pipeline.
        if request.POST is not None:
            pipeline.bind_state('user_input', request.POST)
            return pipeline.next_step()

        # Render form template
        return render_view('app/pipeline_input.html')

The pipeline views are declared within the executing pipeline provider's get_pipeline_views method, for example it could look like:

python
def get_pipeline_views(self) -> list[PipelineView]:
    return [GetUserInput(), RequestApiTokenStep()]

Executing a pipeline

Executing a pipeline is done through either one or two views.

With one view it will both initialize the pipeline, and traverse through the pipeline. The downside of this approach is that the provider key, and potentially other information must be known at request time of each step, which may not always be possible (think strict oAuth redirect URL that cannot be parameterized)

This may look something like

python
def handle_request(self, request, organization, provider_key):
    pipeline = MyPipeline(
        request=request,
        organization=organization,
        provider_key=provider_key,
    )

    # Since we are handling initialization in the same view as moving through
    # the pipeline, we have to check if we already have a valid pipeline, to
    # avoid re-initialization.
    if not pipeline.is_valid():
        pipeline.initialize()

    return pipeline.current_step()

Using a second view would remove the is_valid check from the initialization view, and then the second view would lookup the pipeline using it's get_for_request method. Which would look something like:

python
def handle_request(self, request):
    pipeline = MyPipeline.get_for_request(request)

    # If the pipeline isn't correctly initialized error out
    if pipeline is None or not pipeline.is_valid():
        return HttpResponse(code=400)

    # Pipeline is already initialized
    return pipeline.current_step()

The advantage of a view specifically for traversing through the view is that if the user does not complete a pipeline, and later comes back to traverse through the same pipeline from the start, the initialize view will clear the pipeline state.

Nested Pipeline Views

Nested Pipelines is a pipeline concept and utility class to support composition of pipelines together. For example, if you want to include an entire other pipeline as steps within another pipeline, you can do this using the NestedPipelineView.

  • The NestedPipelineView is itself a PipelineView and should be used directly in the list of pipeline views returned by a provider.

  • When a nested pipeline completes, it very importantly does not call the finish_pipeline method on the pipeline itself, instead the state is bound into the parent pipeline.

  • Nested pipelines use the "single view"

An example of a nested pipeline looks like:

python
def get_pipeline_views(self):
    identity_pipeline_config = {
        'oauth_scopes': self.identity_oauth_scopes,
        'redirect_url': absolute_uri('/extensions/slack/setup/'),
    }

    identity_pipeline_view = NestedPipelineView(
        bind_key='identity',
        provider_key='slack',
        pipeline_cls=IdentityPipeline,
        config=identity_pipeline_config,
    )

    return [identity_pipeline_view]