Import sentry-python_0.12.2.orig.tar.gz

[dgit import orig sentry-python_0.12.2.orig.tar.gz]
This commit is contained in:
William Grzybowski 2019-09-23 21:44:48 +02:00
commit f5b4c23861
146 changed files with 17900 additions and 0 deletions

16
.craft.yml Normal file
View File

@ -0,0 +1,16 @@
---
minVersion: '0.5.1'
github:
owner: getsentry
repo: sentry-python
targets:
- name: pypi
- name: github
- name: gh-pages
- name: registry
type: sdk
config:
canonical: pypi:sentry-sdk
changelog: CHANGES.md
changelogPolicy: simple

6
.flake8 Normal file
View File

@ -0,0 +1,6 @@
[flake8]
ignore = E203, E266, E501, W503, E402, E731, C901, B950
max-line-length = 80
max-complexity = 18
select = B,C,E,F,W,T4,B9
exclude=checkouts,lol*,.tox

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
*.pyc
*.log
*.egg
*.db
*.pid
.python-version
.coverage*
.DS_Store
.tox
pip-log.txt
*.egg-info
/build
/dist
.cache
.idea
.eggs
venv
.venv
.vscode/tags
.pytest_cache
.hypothesis
semaphore
pip-wheel-metadata
.mypy_cache

71
.travis.yml Normal file
View File

@ -0,0 +1,71 @@
language: python
python:
- "2.7"
- "pypy"
- "3.4"
- "3.5"
- "3.6"
env:
- SENTRY_PYTHON_TEST_POSTGRES_USER=postgres SENTRY_PYTHON_TEST_POSTGRES_NAME=travis_ci_test
cache:
pip: true
cargo: true
branches:
only:
- master
- /^release\/.+$/
matrix:
allow_failures:
- python: "3.8-dev"
include:
- python: "3.7"
dist: xenial
- python: "3.8-dev"
dist: xenial
- name: Linting
python: "3.6"
install:
- pip install tox
script: tox -e linters
- python: "3.6"
name: Distribution packages
install: false
script: make travis-upload-dist
- python: "3.6"
name: Build documentation
install: false
script: make travis-upload-docs
before_script:
- psql -c 'create database travis_ci_test;' -U postgres
- psql -c 'create database test_travis_ci_test;' -U postgres
services:
- postgresql
install:
- pip install tox
- pip install codecov
- bash scripts/download-semaphore.sh
script:
- coverage erase
- ./scripts/runtox.sh '' --cov=sentry_sdk --cov-report= --cov-branch
- codecov --file .coverage*
notifications:
webhooks:
urls:
- https://zeus.ci/hooks/7ebb3060-90d8-11e8-aa04-0a580a282e07/public/provider/travis/webhook
on_success: always
on_failure: always
on_start: always
on_cancel: always
on_error: always

425
CHANGES.md Normal file
View File

@ -0,0 +1,425 @@
# Changelog and versioning
## Versioning Policy
This project follows [semver](https://semver.org/), with three additions:
* Semver says that major version `0` can include breaking changes at any time.
Still, it is common practice to assume that only `0.x` releases (minor
versions) can contain breaking changes while `0.x.y` releases (patch
versions) are used for backwards-compatible changes (bugfixes and features).
This project also follows that practice.
* All undocumented APIs are considered internal. They are not part of this
contract.
* Certain features (e.g. integrations) may be explicitly called out as
"experimental" or "unstable" in the documentation. They come with their own
versioning policy described in the documentation.
We recommend to pin your version requirements against `0.x.*` or `0.x.y`.
Either one of the following is fine:
```
sentry-sdk>=0.10.0,<0.11.0
sentry-sdk==0.10.1
```
A major release `N` implies the previous release `N-1` will no longer receive updates. We generally do not backport bugfixes to older versions unless they are security relevant. However, feel free to ask for backports of specific commits on the bugtracker.
## 0.12.2
* Fix a crash with ASGI (Django Channels) when the ASGI request type is neither HTTP nor Websockets.
## 0.12.1
* Temporarily remove sending of SQL parameters (as part of breadcrumbs or spans for APM) to Sentry to avoid memory consumption issues.
## 0.12.0
* Sentry now has a [Discord server](https://discord.gg/cWnMQeA)! Join the server to get involved into SDK development and ask questions.
* Fix a bug where the response object for httplib (or requests) was held onto for an unnecessarily long amount of time.
* APM: Add spans for more methods on `subprocess.Popen` objects.
* APM: Add spans for Django middlewares.
* APM: Add spans for ASGI requests.
* Automatically inject the ASGI middleware for Django Channels 2.0. This will **break your Channels 2.0 application if it is running on Python 3.5 or 3.6** (while previously it would "only" leak a lot of memory for each ASGI request). **Install `aiocontextvars` from PyPI to make it work again.**
## 0.11.2
* Fix a bug where the SDK would throw an exception on shutdown when running under eventlet.
* Add missing data to Redis breadcrumbs.
## 0.11.1
* Remove a faulty assertion (observed in environment with Django Channels and ASGI).
## 0.11.0
* Fix type hints for the logging integration. Thansk Steven Dignam!
* Fix an issue where scope/context data would leak in applications that use `gevent` with its threading monkeypatch. The fix is to avoid usage of contextvars in such environments. Thanks Ran Benita!
* Fix a reference cycle in the `ThreadingIntegration` that led to exceptions on interpreter shutdown. Thanks Guang Tian Li!
* Fix a series of bugs in the stdlib integration that broke usage of `subprocess`.
* More instrumentation for APM.
* New integration for SQLAlchemy (creates breadcrumbs from queries).
* New (experimental) integration for Apache Beam.
* Fix a bug in the `LoggingIntegration` that would send breadcrumbs timestamps in the wrong timezone.
* The `AiohttpIntegration` now sets the event's transaction name.
* Fix a bug that caused infinite recursion when serializing local variables that logged errors or otherwise created Sentry events.
## 0.10.2
* Fix a bug where a log record with non-strings as `extra` keys would make the SDK crash.
* Added ASGI integration for better hub propagation, request data for your events and capturing uncaught exceptions. Using this middleware explicitly in your code will also fix a few issues with Django Channels.
* Fix a bug where `celery-once` was deadlocking when used in combination with the celery integration.
* Fix a memory leak in the new tracing feature when it is not enabled.
## 0.10.1
* Fix bug where the SDK would yield a deprecation warning about
`collections.abc` vs `collections`.
* Fix bug in stdlib integration that would cause spawned subprocesses to not
inherit the environment variables from the parent process.
## 0.10.0
* Massive refactor in preparation to tracing. There are no intentional breaking
changes, but there is a risk of breakage (hence the minor version bump). Two
new client options `traces_sample_rate` and `traceparent_v2` have been added.
Do not change the defaults in production, they will bring your application
down or at least fill your Sentry project up with nonsense events.
## 0.9.5
* Do not use ``getargspec`` on Python 3 to evade deprecation
warning.
## 0.9.4
* Revert a change in 0.9.3 that prevented passing a ``unicode``
string as DSN to ``init()``.
## 0.9.3
* Add type hints for ``init()``.
* Include user agent header when sending events.
## 0.9.2
* Fix a bug in the Django integration that would prevent the user
from initializing the SDK at the top of `settings.py`.
This bug was introduced in 0.9.1 for all Django versions, but has been there
for much longer for Django 1.6 in particular.
## 0.9.1
* Fix a bug on Python 3.7 where gunicorn with gevent would cause the SDK to
leak event data between requests.
* Fix a bug where the GNU backtrace integration would not parse certain frames.
* Fix a bug where the SDK would not pick up request bodies for Django Rest
Framework based apps.
* Remove a few more headers containing sensitive data per default.
* Various improvements to type hints. Thanks Ran Benita!
* Add a event hint to access the log record from `before_send`.
* Fix a bug that would ignore `__tracebackhide__`. Thanks Matt Millican!
* Fix distribution information for mypy support (add `py.typed` file). Thanks
Ran Benita!
## 0.9.0
* The SDK now captures `SystemExit` and other `BaseException`s when coming from
within a WSGI app (Flask, Django, ...)
* Pyramid: No longer report an exception if there exists an exception view for
it.
## 0.8.1
* Fix infinite recursion bug in Celery integration.
## 0.8.0
* Add the always_run option in excepthook integration.
* Fix performance issues when attaching large data to events. This is not
really intended to be a breaking change, but this release does include a
rewrite of a larger chunk of code, therefore the minor version bump.
## 0.7.14
* Fix crash when using Celery integration (`TypeError` when using
`apply_async`).
## 0.7.13
* Fix a bug where `Ignore` raised in a Celery task would be reported to Sentry.
* Add experimental support for tracing PoC.
## 0.7.12
* Read from `X-Real-IP` for user IP address.
* Fix a bug that would not apply in-app rules for attached callstacks.
* It's now possible to disable automatic proxy support by passing
`http_proxy=""`. Thanks Marco Neumann!
## 0.7.11
* Fix a bug that would send `errno` in an invalid format to the server.
* Fix import-time crash when running Python with `-O` flag.
* Fix a bug that would prevent the logging integration from attaching `extra`
keys called `data`.
* Fix order in which exception chains are reported to match Raven behavior.
* New integration for the Falcon web framework. Thanks to Jacob Magnusson!
## 0.7.10
* Add more event trimming.
* Log Sentry's response body in debug mode.
* Fix a few bad typehints causing issues in IDEs.
* Fix a bug in the Bottle integration that would report HTTP exceptions (e.g.
redirects) as errors.
* Fix a bug that would prevent use of `in_app_exclude` without
setting `in_app_include`.
* Fix a bug where request bodies of Django Rest Framework apps were not captured.
* Suppress errors during SQL breadcrumb capturing in Django
integration. Also change order in which formatting strategies
are tried.
## 0.7.9
* New integration for the Bottle web framework. Thanks to Stepan Henek!
* Self-protect against broken mapping implementations and other broken reprs
instead of dropping all local vars from a stacktrace. Thanks to Marco
Neumann!
## 0.7.8
* Add support for Sanic versions 18 and 19.
* Fix a bug that causes an SDK crash when using composed SQL from psycopg2.
## 0.7.7
* Fix a bug that would not capture request bodies if they were empty JSON
arrays, objects or strings.
* New GNU backtrace integration parses stacktraces from exception messages and
appends them to existing stacktrace.
* Capture Tornado formdata.
* Support Python 3.6 in Sanic and AIOHTTP integration.
* Clear breadcrumbs before starting a new request.
* Fix a bug in the Celery integration that would drop pending events during
worker shutdown (particularly an issue when running with `max_tasks_per_child
= 1`)
* Fix a bug with `repr`ing locals whose `__repr__` simultaneously changes the
WSGI environment or other data that we're also trying to serialize at the
same time.
## 0.7.6
* Fix a bug where artificial frames for Django templates would not be marked as
in-app and would always appear as the innermost frame. Implement a heuristic
to show template frame closer to `render` or `parse` invocation.
## 0.7.5
* Fix bug into Tornado integration that would send broken cookies to the server.
* Fix a bug in the logging integration that would ignore the client
option `with_locals`.
## 0.7.4
* Read release and environment from process environment like the Raven SDK
does. The keys are called `SENTRY_RELEASE` and `SENTRY_ENVIRONMENT`.
* Fix a bug in the `serverless` integration where it would not push a new scope
for each function call (leaking tags and other things across calls).
* Experimental support for type hints.
## 0.7.3
* Fix crash in AIOHTTP integration when integration was set up but disabled.
* Flask integration now adds usernames, email addresses based on the protocol
Flask-User defines on top of Flask-Login.
* New threading integration catches exceptions from crashing threads.
* New method `flush` on hubs and clients. New global `flush` function.
* Add decorator for serverless functions to fix common problems in those
environments.
* Fix a bug in the logging integration where using explicit handlers required
enabling the integration.
## 0.7.2
* Fix `celery.exceptions.Retry` spamming in Celery integration.
## 0.7.1
* Fix `UnboundLocalError` crash in Celery integration.
## 0.7.0
* Properly display chained exceptions (PEP-3134).
* Rewrite celery integration to monkeypatch instead of using signals due to
bugs in Celery 3's signal handling. The Celery scope is also now available in
prerun and postrun signals.
* Fix Tornado integration to work with Tornado 6.
* Do not evaluate Django `QuerySet` when trying to capture local variables.
Also an internal hook was added to overwrite `repr` for local vars.
## 0.6.9
* Second attempt at fixing the bug that was supposed to be fixed in 0.6.8.
> No longer access arbitrary sequences in local vars due to possible side effects.
## 0.6.8
* No longer access arbitrary sequences in local vars due to possible side effects.
## 0.6.7
* Sourcecode Django templates is now displayed in stackframes like Jinja templates in Flask already were.
* Updates to AWS Lambda integration for changes Amazon did to their Python 3.7 runtime.
* Fix a bug in the AIOHTTP integration that would report 300s and other HTTP status codes as errors.
* Fix a bug where a crashing `before_send` would crash the SDK and app.
* Fix a bug where cyclic references in e.g. local variables or `extra` data would crash the SDK.
## 0.6.6
* Un-break API of internal `Auth` object that we use in Sentry itself.
## 0.6.5
* Capture WSGI request data eagerly to save memory and avoid issues with uWSGI.
* Ability to use subpaths in DSN.
* Ignore `django.request` logger.
## 0.6.4
* Fix bug that would lead to an `AssertionError: stack must have at least one layer`, at least in testsuites for Flask apps.
## 0.6.3
* New integration for Tornado
* Fix request data in Django, Flask and other WSGI frameworks leaking between events.
* Fix infinite recursion when sending more events in `before_send`.
## 0.6.2
* Fix crash in AWS Lambda integration when using Zappa. This only silences the error, the underlying bug is still in Zappa.
## 0.6.1
* New integration for aiohttp-server.
* Fix crash when reading hostname in broken WSGI environments.
## 0.6.0
* Fix bug where a 429 without Retry-After would not be honored.
* Fix bug where proxy setting would not fall back to `http_proxy` for HTTPs traffic.
* A WSGI middleware is now available for catching errors and adding context about the current request to them.
* Using `logging.debug("test", exc_info=True)` will now attach the current stacktrace if no `sys.exc_info` is available.
* The Python 3.7 runtime for AWS Lambda is now supported.
* Fix a bug that would drop an event or parts of it when it contained bytes that were not UTF-8 encoded.
* Logging an exception will no longer add the exception as breadcrumb to the exception's own event.
## 0.5.5
* New client option `ca_certs`.
* Fix crash with Django and psycopg2.
## 0.5.4
* Fix deprecation warning in relation to the `collections` stdlib module.
* Fix bug that would crash Django and Flask when streaming responses are failing halfway through.
## 0.5.3
* Fix bug where using `push_scope` with a callback would not pop the scope.
* Fix crash when initializing the SDK in `push_scope`.
* Fix bug where IP addresses were sent when `send_default_pii=False`.
## 0.5.2
* Fix bug where events sent through the RQ integration were sometimes lost.
* Remove a deprecation warning about usage of `logger.warn`.
* Fix bug where large frame local variables would lead to the event being rejected by Sentry.
## 0.5.1
* Integration for Redis Queue (RQ)
## 0.5.0
* Fix a bug that would omit several debug logs during SDK initialization.
* Fix issue that sent a event key `""` Sentry wouldn't understand.
* **Breaking change:** The `level` and `event_level` options in the logging integration now work separately from each other.
* Fix a bug in the Sanic integration that would report the exception behind any HTTP error code.
* Fix a bug that would spam breadcrumbs in the Celery integration. Ignore logger `celery.worker.job`.
* Additional attributes on log records are now put into `extra`.
* Integration for Pyramid.
* `sys.argv` is put into extra automatically.
## 0.4.3
* Fix a bug that would leak WSGI responses.
## 0.4.2
* Fix a bug in the Sanic integration that would leak data between requests.
* Fix a bug that would hide all debug logging happening inside of the built-in transport.
* Fix a bug that would report errors for typos in Django's shell.
## 0.4.1
* Fix bug that would only show filenames in stacktraces but not the parent
directories.
## 0.4.0
* Changed how integrations are initialized. Integrations are now
configured and enabled per-client.
## 0.3.11
* Fix issue with certain deployment tools and the AWS Lambda integration.
## 0.3.10
* Set transactions for Django like in Raven. Which transaction behavior is used
can be configured.
* Fix a bug which would omit frame local variables from stacktraces in Celery.
* New option: `attach_stacktrace`
## 0.3.9
* Bugfixes for AWS Lambda integration: Using Zappa did not catch any exceptions.
## 0.3.8
* Nicer log level for internal errors.
## 0.3.7
* Remove `repos` configuration option. There was never a way to make use of
this feature.
* Fix a bug in `last_event_id`.
* Add Django SQL queries to breadcrumbs.
* Django integration won't set user attributes if they were already set.
* Report correct SDK version to Sentry.
## 0.3.6
* Integration for Sanic
## 0.3.5
* Integration for AWS Lambda
* Fix mojibake when encoding local variable values
## 0.3.4
* Performance improvement when storing breadcrumbs
## 0.3.3
* Fix crash when breadcrumbs had to be trunchated
## 0.3.2
* Fixed an issue where some paths where not properly sent as absolute paths

60
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,60 @@
# How to contribute to the Sentry Python SDK
`sentry-sdk` is an ordinary Python package. You can install it with `pip
install -e .` into some virtualenv, edit the sourcecode and test out your
changes manually.
## Community
The public-facing channels for support and development of Sentry SDKs can be found on [Discord](https://discord.gg/Ww9hbqr).
## Running tests and linters
Make sure you have `virtualenv` installed, and the Python versions you care
about. You should have Python 2.7 and the latest Python 3 installed.
You don't need to `workon` or `activate` anything, the `Makefile` will create
one for you. Run `make` or `make help` to list commands.
## Releasing a new version
We use [craft](https://github.com/getsentry/craft#python-package-index-pypi) to
release new versions. You need credentials for the `getsentry` PyPI user, and
must have `twine` installed globally.
The usual release process goes like this:
1. Go through git log and write new entry into `CHANGES.md`, commit to master
2. `craft p a.b.c`
3. `craft pp a.b.c`
## Adding a new integration (checklist)
1. Write the integration.
* Instrument all application instances by default. Prefer global signals/patches instead of configuring a specific instance. Don't make the user pass anything to your integration for anything to work. Aim for zero configuration.
* Everybody monkeypatches. That means:
* Make sure to think about conflicts with other monkeypatches when monkeypatching.
* You don't need to feel bad about it.
* Avoid modifying the hub, registering a new client or the like. The user drives the client, and the client owns integrations.
* Allow the user to disable the integration by changing the client. Check `Hub.current.get_integration(MyIntegration)` from within your signal handlers to see if your integration is still active before you do anything impactful (such as sending an event).
2. Write the [docs](https://github.com/getsentry/sentry-docs). Answer the following questions:
* What does your integration do? Split in two sections: Executive summary at top and exact behavior further down.
* Which version of the SDK supports which versions of the modules it hooks into?
* One code example with basic setup.
* Make sure to add integration page to `python/index.md` (people forget to do that all the time).
Tip: Put most relevant parts wrapped in `<!--WIZARD-->..<!--ENDWIZARD-->` tags for usage from within the Sentry UI.
3. Merge docs after new version has been released (auto-deploys on merge).
4. (optional) Update data in [`sdk_updates.py`](https://github.com/getsentry/sentry/blob/master/src/sentry/sdk_updates.py) to give users in-app suggestions to use your integration. May not be applicable or doable for all kinds of integrations.

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
Copyright (c) 2018 Sentry (https://sentry.io) and individual contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

2
MANIFEST.in Normal file
View File

@ -0,0 +1,2 @@
include LICENSE
include sentry_sdk/py.typed

75
Makefile Normal file
View File

@ -0,0 +1,75 @@
SHELL = /bin/bash
VENV_PATH = .venv
help:
@echo "Thanks for your interest in the Sentry Python SDK!"
@echo
@echo "make lint: Run linters"
@echo "make test: Run basic tests (not testing most integrations)"
@echo "make test-all: Run ALL tests (slow, closest to CI)"
@echo "make format: Run code formatters (destructive)"
@echo
@echo "Also make sure to read ./CONTRIBUTING.md"
@false
.venv:
virtualenv -ppython3 $(VENV_PATH)
$(VENV_PATH)/bin/pip install tox
dist: .venv
rm -rf dist build
$(VENV_PATH)/bin/python setup.py sdist bdist_wheel
.PHONY: dist
format: .venv
$(VENV_PATH)/bin/tox -e linters --notest
.tox/linters/bin/black .
.PHONY: format
test: .venv
@$(VENV_PATH)/bin/tox -e py2.7,py3.7
.PHONY: test
test-all: .venv
@TOXPATH=$(VENV_PATH)/bin/tox sh ./scripts/runtox.sh
.PHONY: test-all
check: lint test
.PHONY: check
lint: .venv
@set -e && $(VENV_PATH)/bin/tox -e linters || ( \
echo "================================"; \
echo "Bad formatting? Run: make format"; \
echo "================================"; \
false)
.PHONY: lint
apidocs: .venv
@$(VENV_PATH)/bin/pip install --editable .
@$(VENV_PATH)/bin/pip install -U -r ./docs-requirements.txt
@$(VENV_PATH)/bin/sphinx-build -W -b html docs/ docs/_build
.PHONY: apidocs
apidocs-hotfix: apidocs
@$(VENV_PATH)/bin/pip install ghp-import
@$(VENV_PATH)/bin/ghp-import -pf docs/_build
.PHONY: apidocs-hotfix
install-zeus-cli:
npm install -g @zeus-ci/cli
.PHONY: install-zeus-cli
travis-upload-docs: apidocs install-zeus-cli
cd docs/_build && zip -r gh-pages ./
zeus upload -t "application/zip+docs" docs/_build/gh-pages.zip \
|| [[ ! "$(TRAVIS_BRANCH)" =~ ^release/ ]]
.PHONY: travis-upload-docs
travis-upload-dist: dist install-zeus-cli
zeus upload -t "application/zip+wheel" dist/* \
|| [[ ! "$(TRAVIS_BRANCH)" =~ ^release/ ]]
.PHONY: travis-upload-dist

42
README.md Normal file
View File

@ -0,0 +1,42 @@
<p align="center">
<a href="https://sentry.io" target="_blank" align="center">
<img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" width="280">
</a>
</p>
# sentry-python - Sentry SDK for Python
[![Build Status](https://travis-ci.com/getsentry/sentry-python.svg?branch=master)](https://travis-ci.com/getsentry/sentry-python)
[![PyPi page link -- version](https://img.shields.io/pypi/v/sentry-sdk.svg)](https://pypi.python.org/pypi/sentry-sdk)
[![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/cWnMQeA)
This is the next line of the Python SDK for [Sentry](http://sentry.io/), intended to replace the `raven` package on PyPI.
```python
from sentry_sdk import init, capture_message
init("mydsn@sentry.io/123")
capture_message("Hello World") # Will create an event.
raise ValueError() # Will also create an event.
```
To learn more about how to use the SDK:
- [Getting started with the new SDK](https://docs.sentry.io/quickstart/?platform=python)
- [Configuration options](https://docs.sentry.io/error-reporting/configuration/?platform=python)
- [Setting context (tags, user, extra information)](https://docs.sentry.io/enriching-error-data/context/?platform=python)
- [Integrations](https://docs.sentry.io/platforms/python/)
Are you coming from raven-python?
- [Cheatsheet: Migrating to the new SDK from Raven](https://forum.sentry.io/t/switching-to-sentry-python/4733)
To learn about internals:
- [API Reference](https://getsentry.github.io/sentry-python/)
# License
Licensed under the BSD license, see `LICENSE`

79
azure-pipelines.yml Normal file
View File

@ -0,0 +1,79 @@
# Python package
# Create and test a Python package on multiple Python versions.
# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/python
trigger:
- master
pr: none
resources:
containers:
- container: postgres
image: "postgres:9.6"
ports:
- 5432:5432
jobs:
- job: run_tests
displayName: Tests
pool:
vmImage: "Ubuntu-16.04"
services:
postgres: postgres
strategy:
matrix:
Python27:
python.version: "2.7"
Python34:
python.version: "3.4"
Python35:
python.version: "3.5"
Python36:
python.version: "3.6"
Python37:
python.version: "3.7"
# Python 3.8 and PyPy will be soon added to the base VM image:
# https://github.com/Microsoft/azure-pipelines-tasks/pull/9866
Python38:
python.version: "3.8-dev"
PyPy2:
python.version: "pypy2"
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: "$(python.version)"
architecture: "x64"
- script: |
set -eux
docker ps -a
docker images -a
# FIXME: theoretically we can run psql commands from a docker container, but
# name resolution is a bit tricky here
sudo apt install -y postgresql-client
psql -c 'create database travis_ci_test;' -U postgres -h localhost
psql -c 'create database test_travis_ci_test;' -U postgres -h localhost
displayName: "Create Postgres users"
- script: |
set -eux
python --version
pip --version
pip install tox
pip install codecov
sh scripts/download-semaphore.sh
displayName: "Install dependencies"
- script: |
set -eux
coverage erase
./scripts/runtox.sh '' --cov=sentry_sdk --cov-report= --cov-branch
codecov --file .coverage*
env:
SENTRY_PYTHON_TEST_POSTGRES_USER: postgres
SENTRY_PYTHON_TEST_POSTGRES_NAME: travis_ci_test
AZURE_PYTHON_VERSION: "$(python.version)"
displayName: "Run tests"

4
docs-requirements.txt Normal file
View File

@ -0,0 +1,4 @@
sphinx
sphinx-rtd-theme
git+https://github.com/agronholm/sphinx-autodoc-typehints
typed_ast

1
docs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_build

0
docs/_static/.gitkeep vendored Normal file
View File

9
docs/api.rst Normal file
View File

@ -0,0 +1,9 @@
========
Main API
========
.. inherited-members necessary because of hack for Client and init methods
.. automodule:: sentry_sdk
:members:
:inherited-members:

185
docs/conf.py Normal file
View File

@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
import os
import sys
import typing
typing.TYPE_CHECKING = True
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
sys.path.insert(0, os.path.abspath(".."))
# -- Project information -----------------------------------------------------
project = u"sentry-python"
copyright = u"2019, Sentry Team and Contributors"
author = u"Sentry Team and Contributors"
release = "0.12.2"
version = ".".join(release.split(".")[:2]) # The short X.Y version.
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx_autodoc_typehints",
"sphinx.ext.viewcode",
"sphinx.ext.githubpages",
"sphinx.ext.intersphinx",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"
# The master toctree document.
master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = [u"_build", "Thumbs.db", ".DS_Store"]
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
on_rtd = os.environ.get("READTHEDOCS", None) == "True"
html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = "sentry-pythondoc"
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(
master_doc,
"sentry-python.tex",
u"sentry-python Documentation",
u"Sentry Team and Contributors",
"manual",
)
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "sentry-python", u"sentry-python Documentation", [author], 1)]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"sentry-python",
u"sentry-python Documentation",
author,
"sentry-python",
"One line description of project.",
"Miscellaneous",
)
]
# -- Options for Epub output -------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''
# A unique identification for the text.
#
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ["search.html"]
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}

11
docs/index.rst Normal file
View File

@ -0,0 +1,11 @@
=====================================
sentry-python - Sentry SDK for Python
=====================================
This is the API documentation for `Sentry's Python SDK
<https://sentry.io/for/python/>`_. For full documentation and other resources
visit the `GitHub repository <https://github.com/getsentry/sentry-python>`_.
.. toctree::
api
integrations

14
docs/integrations.rst Normal file
View File

@ -0,0 +1,14 @@
============
Integrations
============
Logging
=======
.. module:: sentry_sdk.integrations.logging
.. autofunction:: ignore_logger
.. autoclass:: EventHandler
.. autoclass:: BreadcrumbHandler

35
examples/basic.py Normal file
View File

@ -0,0 +1,35 @@
import sentry_sdk
from sentry_sdk.integrations.excepthook import ExcepthookIntegration
from sentry_sdk.integrations.atexit import AtexitIntegration
from sentry_sdk.integrations.dedupe import DedupeIntegration
from sentry_sdk.integrations.stdlib import StdlibIntegration
sentry_sdk.init(
dsn="https://<key>@sentry.io/<project>",
default_integrations=False,
integrations=[
ExcepthookIntegration(),
AtexitIntegration(),
DedupeIntegration(),
StdlibIntegration(),
],
environment="Production",
release="1.0.0",
send_default_pii=False,
max_breadcrumbs=5,
)
with sentry_sdk.push_scope() as scope:
scope.user = {"email": "john.doe@example.com"}
scope.set_tag("page_locale", "de-at")
scope.set_extra("request", {"id": "d5cf8a0fd85c494b9c6453c4fba8ab17"})
scope.level = "warning"
sentry_sdk.capture_message("Something went wrong!")
sentry_sdk.add_breadcrumb(category="auth", message="Authenticated user", level="info")
try:
1 / 0
except Exception as e:
sentry_sdk.capture_exception(e)

View File

@ -0,0 +1,14 @@
To run this app:
1. Have a Redis on the Redis default port (if you have Sentry running locally,
you probably already have this)
2. `pip install sentry-sdk flask rq`
3. `FLASK_APP=tracing flask run`
4. `FLASK_APP=tracing flask worker`
5. Go to `http://localhost:5000/` and enter a base64-encoded string (one is prefilled)
6. Hit submit, wait for heavy computation to end
7. `cat events | python traceviewer.py | dot -T svg > events.svg`
8. `open events.svg`
The last two steps are for viewing the traces. Nothing gets sent to Sentry
right now because Sentry does not deal with this data yet.

10
examples/tracing/events Normal file
View File

@ -0,0 +1,10 @@
{"start_timestamp": "2019-06-14T14:01:38Z", "transaction": "index", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "span_id": "968cff94913ebb07"}}, "timestamp": "2019-06-14T14:01:38Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Upgrade-Insecure-Requests": "1", "Connection": "keep-alive", "Pragma": "no-cache", "Cache-Control": "no-cache", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"}}, "event_id": "f9f4b21dd9da4c389426c1ffd2b62410", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}}
{"start_timestamp": "2019-06-14T14:01:38Z", "transaction": "static", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"trace_id": "8eb30d5ae5f3403ba3a036e696111ec3", "span_id": "97e894108ff7a8cd"}}, "timestamp": "2019-06-14T14:01:38Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/static/tracing.js", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0", "Connection": "keep-alive", "Referer": "http://127.0.0.1:5000/", "Pragma": "no-cache", "Cache-Control": "no-cache"}}, "event_id": "1c71c7cb32934550bb49f05b6c2d4052", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}}
{"start_timestamp": "2019-06-14T14:01:38Z", "transaction": "index", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"trace_id": "b7627895a90b41718be82d3ad21ab2f4", "span_id": "9fa95b4ffdcbe177"}}, "timestamp": "2019-06-14T14:01:38Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0", "Connection": "keep-alive", "Pragma": "no-cache", "Cache-Control": "no-cache"}}, "event_id": "1430ad5b0a0d45dca3f02c10271628f9", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}}
{"start_timestamp": "2019-06-14T14:01:38Z", "transaction": "static", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"trace_id": "1636fdb33db84e7c9a4e606c1b176971", "span_id": "b682a29ead55075f"}}, "timestamp": "2019-06-14T14:01:38Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/static/tracing.js.map", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0", "Connection": "keep-alive"}}, "event_id": "72b1224307294e0fb6d6b1958076c4cc", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}}
{"start_timestamp": "2019-06-14T14:01:40Z", "transaction": "compute", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"parent_span_id": "bce14471e0e9654d", "trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "span_id": "946edde6ee421874"}}, "timestamp": "2019-06-14T14:01:40Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/compute/aGVsbG8gd29ybGQK", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "Sentry-Trace": "00-a0fa8803753e40fd8124b21eeb2986b5-bce14471e0e9654d-00", "Connection": "keep-alive", "Referer": "http://127.0.0.1:5000/", "Pragma": "no-cache", "Cache-Control": "no-cache", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"}}, "event_id": "c72fd945c1174140a00bdbf6f6ed8fc5", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}}
{"start_timestamp": "2019-06-14T14:01:40Z", "transaction": "wait", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"parent_span_id": "bce14471e0e9654d", "trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "span_id": "bf5be759039ede9a"}}, "timestamp": "2019-06-14T14:01:40Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/wait/sentry-python-tracing-example-result:aGVsbG8gd29ybGQK", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "Sentry-Trace": "00-a0fa8803753e40fd8124b21eeb2986b5-bce14471e0e9654d-00", "Connection": "keep-alive", "Referer": "http://127.0.0.1:5000/", "Pragma": "no-cache", "Cache-Control": "no-cache", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"}}, "event_id": "e8c17b0cbe2045758aaffc2f11672fab", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}}
{"start_timestamp": "2019-06-14T14:01:40Z", "transaction": "wait", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"parent_span_id": "bce14471e0e9654d", "trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "span_id": "b2d56249f7fdf327"}}, "timestamp": "2019-06-14T14:01:40Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/wait/sentry-python-tracing-example-result:aGVsbG8gd29ybGQK", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "Sentry-Trace": "00-a0fa8803753e40fd8124b21eeb2986b5-bce14471e0e9654d-00", "Connection": "keep-alive", "Referer": "http://127.0.0.1:5000/", "Pragma": "no-cache", "Cache-Control": "no-cache", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"}}, "event_id": "6577f8056383427d85df5b33bf9ccc2c", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}}
{"start_timestamp": "2019-06-14T14:01:41Z", "transaction": "wait", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"parent_span_id": "bce14471e0e9654d", "trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "span_id": "ac62ff8ae1b2eda6"}}, "timestamp": "2019-06-14T14:01:41Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/wait/sentry-python-tracing-example-result:aGVsbG8gd29ybGQK", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "Sentry-Trace": "00-a0fa8803753e40fd8124b21eeb2986b5-bce14471e0e9654d-00", "Connection": "keep-alive", "Referer": "http://127.0.0.1:5000/", "Pragma": "no-cache", "Cache-Control": "no-cache", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"}}, "event_id": "c03dfbab8a8145eeaa0d1a1adfcfcaa5", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}}
{"start_timestamp": "2019-06-14T14:01:40Z", "transaction": "tracing.decode_base64", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "worker"], "rq-job": {"kwargs": {"redis_key": "sentry-python-tracing-example-result:aGVsbG8gd29ybGQK", "encoded": "aGVsbG8gd29ybGQK"}, "args": [], "description": "tracing.decode_base64(encoded=u'aGVsbG8gd29ybGQK', redis_key='sentry-python-tracing-example-result:aGVsbG8gd29ybGQK')", "func": "tracing.decode_base64", "job_id": "fabff810-3dbb-45d3-987e-86395790dfa9"}}, "contexts": {"trace": {"parent_span_id": "946edde6ee421874", "trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "span_id": "9c2a6db8c79068a2"}}, "timestamp": "2019-06-14T14:01:41Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "event_id": "2975518984734ef49d2f75db4e928ddc", "platform": "python", "spans": [{"start_timestamp": "2019-06-14T14:01:41Z", "same_process_as_parent": true, "description": "http://httpbin.org/base64/aGVsbG8gd29ybGQK GET", "tags": {"http.status_code": 200, "error": false}, "timestamp": "2019-06-14T14:01:41Z", "parent_span_id": "9c2a6db8c79068a2", "trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "op": "http", "data": {"url": "http://httpbin.org/base64/aGVsbG8gd29ybGQK", "status_code": 200, "reason": "OK", "method": "GET"}, "span_id": "8c931f4740435fb8"}], "breadcrumbs": [{"category": "httplib", "data": {"url": "http://httpbin.org/base64/aGVsbG8gd29ybGQK", "status_code": 200, "reason": "OK", "method": "GET"}, "type": "http", "timestamp": "2019-06-14T12:01:41Z"}, {"category": "rq.worker", "ty": "log", "timestamp": "2019-06-14T14:01:41Z", "level": "info", "data": {"asctime": "14:01:41"}, "message": "\u001b[32mdefault\u001b[39;49;00m: \u001b[34mJob OK\u001b[39;49;00m (fabff810-3dbb-45d3-987e-86395790dfa9)", "type": "default"}, {"category": "rq.worker", "ty": "log", "timestamp": "2019-06-14T14:01:41Z", "level": "info", "data": {"asctime": "14:01:41"}, "message": "Result is kept for 500 seconds", "type": "default"}], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}}
{"start_timestamp": "2019-06-14T14:01:41Z", "transaction": "wait", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"parent_span_id": "bce14471e0e9654d", "trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "span_id": "9d91c6558b2e4c06"}}, "timestamp": "2019-06-14T14:01:41Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/wait/sentry-python-tracing-example-result:aGVsbG8gd29ybGQK", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "Sentry-Trace": "00-a0fa8803753e40fd8124b21eeb2986b5-bce14471e0e9654d-00", "Connection": "keep-alive", "Referer": "http://127.0.0.1:5000/", "Pragma": "no-cache", "Cache-Control": "no-cache", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"}}, "event_id": "339cfc84adf0405986514c808afb0f68", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}}

439
examples/tracing/events.svg Normal file
View File

@ -0,0 +1,439 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: mytrace Pages: 1 -->
<svg width="2724pt" height="1093pt"
viewBox="0.00 0.00 2724.46 1093.13" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1089.1279)">
<title>mytrace</title>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1089.1279 2720.4642,-1089.1279 2720.4642,4 -4,4"/>
<!-- 213977312221895837199412816265326724789 -->
<g id="node1" class="node">
<title>213977312221895837199412816265326724789</title>
<ellipse fill="none" stroke="#000000" cx="193.5483" cy="-513" rx="193.5966" ry="18"/>
<text text-anchor="middle" x="193.5483" y="-508.8" font-family="Times,serif" font-size="14.00" fill="#000000">trace:index (a0fa8803753e40fd8124b21eeb2986b5)</text>
</g>
<!-- 10848326615985732359 -->
<g id="node2" class="node">
<title>10848326615985732359</title>
<ellipse fill="none" stroke="#000000" cx="2593.1455" cy="-680" rx="123.3354" ry="18"/>
<text text-anchor="middle" x="2593.1455" y="-675.8" font-family="Times,serif" font-size="14.00" fill="#000000">span:index (968cff94913ebb07)</text>
</g>
<!-- 213977312221895837199412816265326724789&#45;&gt;10848326615985732359 -->
<g id="edge1" class="edge">
<title>213977312221895837199412816265326724789&#45;&gt;10848326615985732359</title>
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M324.7286,-526.2919C347.9721,-533.9944 370.1309,-545.6118 387.0966,-563 436.4862,-613.6196 380.998,-661.1723 423.0966,-718 508.2525,-832.9498 564.9758,-837.2807 701.8132,-879 833.1298,-919.0361 872.9286,-892.1463 1009.9879,-900 1191.592,-910.4062 1236.9732,-914.7766 1418.8261,-919 1680.07,-925.0671 1749.9495,-967.1823 2006.7834,-919 2023.6564,-915.8346 2026.3092,-909.8286 2042.7834,-905 2212.9335,-855.1287 2275.7903,-906.3909 2433.8267,-826 2492.0895,-796.3626 2544.8443,-739.4376 2572.6002,-706.0977"/>
<polygon fill="#000000" stroke="#000000" points="2575.4736,-708.1132 2579.1062,-698.1606 2570.0599,-703.6756 2575.4736,-708.1132"/>
</g>
<!-- 10695730148961032308 -->
<g id="node9" class="node">
<title>10695730148961032308</title>
<ellipse fill="none" stroke="#000000" cx="837.9006" cy="-536" rx="136.1748" ry="18"/>
<text text-anchor="middle" x="837.9006" y="-531.8" font-family="Times,serif" font-size="14.00" fill="#000000">span:compute (946edde6ee421874)</text>
</g>
<!-- 213977312221895837199412816265326724789&#45;&gt;10695730148961032308 -->
<g id="edge6" class="edge">
<title>213977312221895837199412816265326724789&#45;&gt;10695730148961032308</title>
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M364.0221,-504.4762C453.9229,-501.769 565.9818,-501.216 665.8132,-509 693.3501,-511.1471 723.1421,-515.1988 750.0752,-519.5228"/>
<polygon fill="#000000" stroke="#000000" points="749.8125,-523.0263 760.247,-521.1899 750.9447,-516.1185 749.8125,-523.0263"/>
</g>
<!-- 13788869053623754394 -->
<g id="node11" class="node">
<title>13788869053623754394</title>
<ellipse fill="none" stroke="#000000" cx="2238.3051" cy="-764" rx="119.27" ry="18"/>
<text text-anchor="middle" x="2238.3051" y="-759.8" font-family="Times,serif" font-size="14.00" fill="#000000">span:wait (bf5be759039ede9a)</text>
</g>
<!-- 213977312221895837199412816265326724789&#45;&gt;13788869053623754394 -->
<g id="edge8" class="edge">
<title>213977312221895837199412816265326724789&#45;&gt;13788869053623754394</title>
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M306.5788,-527.6473C334.6706,-535.0591 363.4743,-546.2142 387.0966,-563 410.6092,-579.7079 402.7558,-596.5488 423.0966,-617 528.4658,-722.9416 559.875,-758.3111 701.8132,-805 839.5499,-850.3068 1865.3172,-834.8025 2006.7834,-803 2023.8661,-799.1597 2025.8978,-791.6307 2042.7834,-787 2066.8629,-780.3964 2093.1212,-775.6888 2118.3541,-772.3327"/>
<polygon fill="#000000" stroke="#000000" points="2118.8607,-775.7966 2128.3409,-771.0664 2117.9801,-768.8522 2118.8607,-775.7966"/>
</g>
<!-- 12886313978623292199 -->
<g id="node12" class="node">
<title>12886313978623292199</title>
<ellipse fill="none" stroke="#000000" cx="2238.3051" cy="-444" rx="118.3354" ry="18"/>
<text text-anchor="middle" x="2238.3051" y="-439.8" font-family="Times,serif" font-size="14.00" fill="#000000">span:wait (b2d56249f7fdf327)</text>
</g>
<!-- 213977312221895837199412816265326724789&#45;&gt;12886313978623292199 -->
<g id="edge10" class="edge">
<title>213977312221895837199412816265326724789&#45;&gt;12886313978623292199</title>
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M222.8672,-495.2069C325.9906,-433.8006 683.4148,-230.8727 1009.9879,-164 1189.496,-127.242 2005.7946,-201.11 2006.7834,-202 2078.7952,-266.8164 1973.2995,-349.4807 2042.7834,-417 2064.366,-437.9723 2094.1509,-447.7328 2123.9704,-451.5059"/>
<polygon fill="#000000" stroke="#000000" points="2123.7626,-455.0032 2134.0727,-452.5635 2124.4915,-448.0413 2123.7626,-455.0032"/>
</g>
<!-- 12421771694198418854 -->
<g id="node13" class="node">
<title>12421771694198418854</title>
<ellipse fill="none" stroke="#000000" cx="544.4549" cy="-590" rx="116.5196" ry="18"/>
<text text-anchor="middle" x="544.4549" y="-585.8" font-family="Times,serif" font-size="14.00" fill="#000000">span:wait (ac62ff8ae1b2eda6)</text>
</g>
<!-- 213977312221895837199412816265326724789&#45;&gt;12421771694198418854 -->
<g id="edge12" class="edge">
<title>213977312221895837199412816265326724789&#45;&gt;12421771694198418854</title>
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M269.2578,-529.6131C327.729,-542.4435 408.3312,-560.1302 467.1902,-573.0457"/>
<polygon fill="#000000" stroke="#000000" points="466.6608,-576.5127 477.1786,-575.2374 468.1611,-569.6754 466.6608,-576.5127"/>
</g>
<!-- 10129474377767673784 -->
<g id="node14" class="node">
<title>10129474377767673784</title>
<ellipse fill="none" stroke="#000000" cx="1712.8048" cy="-490" rx="293.9573" ry="18"/>
<text text-anchor="middle" x="1712.8048" y="-485.8" font-family="Times,serif" font-size="14.00" fill="#000000">span:http://httpbin.org/base64/aGVsbG8gd29ybGQK GET (8c931f4740435fb8)</text>
</g>
<!-- 213977312221895837199412816265326724789&#45;&gt;10129474377767673784 -->
<g id="edge14" class="edge">
<title>213977312221895837199412816265326724789&#45;&gt;10129474377767673784</title>
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M278.3831,-496.7679C473.559,-461.6001 969.422,-384.6708 1382.8261,-425 1469.7467,-433.4794 1567.7413,-454.2392 1634.1979,-470.0347"/>
<polygon fill="#000000" stroke="#000000" points="1633.6371,-473.4993 1644.1774,-472.4257 1635.2681,-466.692 1633.6371,-473.4993"/>
</g>
<!-- 11252927259328145570 -->
<g id="node15" class="node">
<title>11252927259328145570</title>
<ellipse fill="none" stroke="#000000" cx="1196.407" cy="-452" rx="186.3383" ry="18"/>
<text text-anchor="middle" x="1196.407" y="-447.8" font-family="Times,serif" font-size="14.00" fill="#000000">span:tracing.decode_base64 (9c2a6db8c79068a2)</text>
</g>
<!-- 213977312221895837199412816265326724789&#45;&gt;11252927259328145570 -->
<g id="edge16" class="edge">
<title>213977312221895837199412816265326724789&#45;&gt;11252927259328145570</title>
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M351.5982,-502.5505C375.5522,-501.002 399.978,-499.4429 423.0966,-498 630.6257,-485.0472 869.5381,-470.962 1026.2355,-461.8364"/>
<polygon fill="#000000" stroke="#000000" points="1026.6571,-465.3179 1036.4368,-461.2426 1026.2503,-458.3297 1026.6571,-465.3179"/>
</g>
<!-- 11354074206287318022 -->
<g id="node16" class="node">
<title>11354074206287318022</title>
<ellipse fill="none" stroke="#000000" cx="544.4549" cy="-536" rx="121.2168" ry="18"/>
<text text-anchor="middle" x="544.4549" y="-531.8" font-family="Times,serif" font-size="14.00" fill="#000000">span:wait (9d91c6558b2e4c06)</text>
</g>
<!-- 213977312221895837199412816265326724789&#45;&gt;11354074206287318022 -->
<g id="edge18" class="edge">
<title>213977312221895837199412816265326724789&#45;&gt;11354074206287318022</title>
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M351.8064,-523.3729C375.7948,-524.9453 400.216,-526.5459 423.2532,-528.0559"/>
<polygon fill="#000000" stroke="#000000" points="423.1762,-531.5583 433.3837,-528.7199 423.6341,-524.5733 423.1762,-531.5583"/>
</g>
<!-- 189680067412161401408211119957991300803 -->
<g id="node3" class="node">
<title>189680067412161401408211119957991300803</title>
<ellipse fill="none" stroke="#000000" cx="2238.3051" cy="-390" rx="191.8062" ry="18"/>
<text text-anchor="middle" x="2238.3051" y="-385.8" font-family="Times,serif" font-size="14.00" fill="#000000">trace:static (8eb30d5ae5f3403ba3a036e696111ec3)</text>
</g>
<!-- 10946161693179750605 -->
<g id="node4" class="node">
<title>10946161693179750605</title>
<ellipse fill="none" stroke="#000000" cx="2593.1455" cy="-417" rx="121.8733" ry="18"/>
<text text-anchor="middle" x="2593.1455" y="-412.8" font-family="Times,serif" font-size="14.00" fill="#000000">span:static (97e894108ff7a8cd)</text>
</g>
<!-- 189680067412161401408211119957991300803&#45;&gt;10946161693179750605 -->
<g id="edge2" class="edge">
<title>189680067412161401408211119957991300803&#45;&gt;10946161693179750605</title>
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M2387.5039,-401.3526C2416.6224,-403.5683 2446.7817,-405.8631 2474.7354,-407.9901"/>
<polygon fill="#000000" stroke="#000000" points="2474.6916,-411.4968 2484.9283,-408.7657 2475.2227,-404.517 2474.6916,-411.4968"/>
</g>
<!-- 243760014067241244567037757667822711540 -->
<g id="node5" class="node">
<title>243760014067241244567037757667822711540</title>
<ellipse fill="none" stroke="#000000" cx="2238.3051" cy="-932" rx="195.5433" ry="18"/>
<text text-anchor="middle" x="2238.3051" y="-927.8" font-family="Times,serif" font-size="14.00" fill="#000000">trace:index (b7627895a90b41718be82d3ad21ab2f4)</text>
</g>
<!-- 11504827122213183863 -->
<g id="node6" class="node">
<title>11504827122213183863</title>
<ellipse fill="none" stroke="#000000" cx="2593.1455" cy="-909" rx="121.3888" ry="18"/>
<text text-anchor="middle" x="2593.1455" y="-904.8" font-family="Times,serif" font-size="14.00" fill="#000000">span:index (9fa95b4ffdcbe177)</text>
</g>
<!-- 243760014067241244567037757667822711540&#45;&gt;11504827122213183863 -->
<g id="edge3" class="edge">
<title>243760014067241244567037757667822711540&#45;&gt;11504827122213183863</title>
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M2398.3373,-921.6271C2423.025,-920.0268 2448.1662,-918.3973 2471.8239,-916.8638"/>
<polygon fill="#000000" stroke="#000000" points="2472.1138,-920.3525 2481.8665,-916.2129 2471.661,-913.3671 2472.1138,-920.3525"/>
</g>
<!-- 29528545588201242414770090507008174449 -->
<g id="node7" class="node">
<title>29528545588201242414770090507008174449</title>
<ellipse fill="none" stroke="#000000" cx="2238.3051" cy="-18" rx="194.0811" ry="18"/>
<text text-anchor="middle" x="2238.3051" y="-13.8" font-family="Times,serif" font-size="14.00" fill="#000000">trace:static (1636fdb33db84e7c9a4e606c1b176971)</text>
</g>
<!-- 13151252664271832927 -->
<g id="node8" class="node">
<title>13151252664271832927</title>
<ellipse fill="none" stroke="#000000" cx="2593.1455" cy="-211" rx="123.1377" ry="18"/>
<text text-anchor="middle" x="2593.1455" y="-206.8" font-family="Times,serif" font-size="14.00" fill="#000000">span:static (b682a29ead55075f)</text>
</g>
<!-- 29528545588201242414770090507008174449&#45;&gt;13151252664271832927 -->
<g id="edge4" class="edge">
<title>29528545588201242414770090507008174449&#45;&gt;13151252664271832927</title>
<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M2380.3108,-30.3528C2398.9103,-35.0744 2417.2448,-41.4422 2433.8267,-50 2496.2634,-82.223 2549.1546,-147.8949 2575.4096,-184.6272"/>
<polygon fill="#000000" stroke="#000000" points="2572.5891,-186.701 2581.2017,-192.8714 2578.3168,-182.6769 2572.5891,-186.701"/>
</g>
<!-- 10695730148961032308&#45;&gt;10848326615985732359 -->
<g id="edge19" class="edge">
<title>10695730148961032308&#45;&gt;10848326615985732359</title>
<path fill="none" stroke="#efefef" d="M883.5631,-553.0153C917.8376,-564.9926 966.1981,-580.2238 1009.9879,-588 1287.5914,-637.2967 2134.3705,-666.6016 2461.8817,-676.3494"/>
<polygon fill="#efefef" stroke="#efefef" points="2461.7932,-679.8482 2471.8925,-676.646 2462.0006,-672.8513 2461.7932,-679.8482"/>
</g>
<!-- 10695730148961032308&#45;&gt;10946161693179750605 -->
<g id="edge20" class="edge">
<title>10695730148961032308&#45;&gt;10946161693179750605</title>
<path fill="none" stroke="#efefef" d="M929.6288,-549.393C1177.5968,-582.3783 1879.8852,-654.4066 2433.8267,-504 2481.1615,-491.1476 2529.8273,-461.7665 2560.7102,-440.7045"/>
<polygon fill="#efefef" stroke="#efefef" points="2563.0954,-443.3096 2569.3175,-434.7343 2559.1058,-437.5578 2563.0954,-443.3096"/>
</g>
<!-- 10695730148961032308&#45;&gt;11504827122213183863 -->
<g id="edge21" class="edge">
<title>10695730148961032308&#45;&gt;11504827122213183863</title>
<path fill="none" stroke="#efefef" d="M869.9101,-553.6102C903.9799,-571.9237 959.7882,-600.7015 1009.9879,-621 1441.2215,-795.3715 1577.9284,-759.8577 2006.7834,-940 2023.4633,-947.0065 2025.2224,-954.6502 2042.7834,-959 2211.4823,-1000.7864 2262.0349,-985.3243 2433.8267,-959 2470.802,-953.3341 2510.9795,-940.677 2541.7371,-929.5158"/>
<polygon fill="#efefef" stroke="#efefef" points="2543.2086,-932.7035 2551.3764,-925.9554 2540.7831,-926.1371 2543.2086,-932.7035"/>
</g>
<!-- 10695730148961032308&#45;&gt;13151252664271832927 -->
<g id="edge22" class="edge">
<title>10695730148961032308&#45;&gt;13151252664271832927</title>
<path fill="none" stroke="#efefef" d="M862.021,-518.1532C894.465,-494.6926 954.511,-453.1635 1009.9879,-425 1183.5468,-336.8906 1228.812,-310.1969 1418.8261,-268 1794.4481,-184.5848 2253.6293,-193.1099 2469.6393,-203.4029"/>
<polygon fill="#efefef" stroke="#efefef" points="2469.5822,-206.9041 2479.7404,-203.894 2469.9222,-199.9124 2469.5822,-206.9041"/>
</g>
<!-- 10695730148961032308&#45;&gt;11252927259328145570 -->
<g id="edge15" class="edge">
<title>10695730148961032308&#45;&gt;11252927259328145570</title>
<path fill="none" stroke="#000000" d="M887.5346,-519.2244C921.6756,-508.1254 968.2206,-493.8555 1009.9879,-484 1034.0048,-478.3329 1059.9849,-473.2356 1084.7061,-468.8617"/>
<polygon fill="#000000" stroke="#000000" points="1085.5932,-472.26 1094.8441,-467.0955 1084.3917,-465.3639 1085.5932,-472.26"/>
</g>
<!-- 13610234804785734989 -->
<g id="node10" class="node">
<title>13610234804785734989</title>
<ellipse fill="none" stroke="#000000" cx="193.5483" cy="-590" rx="98.583" ry="18"/>
<text text-anchor="middle" x="193.5483" y="-585.8" font-family="Times,serif" font-size="14.00" fill="#000000">13610234804785734989</text>
</g>
<!-- 13610234804785734989&#45;&gt;10695730148961032308 -->
<g id="edge5" class="edge">
<title>13610234804785734989&#45;&gt;10695730148961032308</title>
<path fill="none" stroke="#000000" d="M255.1809,-604.1769C346.1912,-622.729 521.3508,-649.2991 665.8132,-617 715.4457,-605.9031 767.6468,-578.8394 801.3604,-559.038"/>
<polygon fill="#000000" stroke="#000000" points="803.569,-561.7958 810.3616,-553.6649 799.9811,-555.7853 803.569,-561.7958"/>
</g>
<!-- 13610234804785734989&#45;&gt;13788869053623754394 -->
<g id="edge7" class="edge">
<title>13610234804785734989&#45;&gt;13788869053623754394</title>
<path fill="none" stroke="#000000" d="M214.8024,-607.5822C292.6233,-670.1112 571.3928,-878 837.9006,-878 837.9006,-878 837.9006,-878 1196.407,-878 1286.4883,-878 1930.1605,-901.3664 2006.7834,-854 2035.5371,-836.2252 2014.8345,-806.0152 2042.7834,-787 2063.2116,-773.1015 2087.6936,-765.2184 2112.3109,-761.0617"/>
<polygon fill="#000000" stroke="#000000" points="2113.1068,-764.4819 2122.4836,-759.5498 2112.0777,-757.558 2113.1068,-764.4819"/>
</g>
<!-- 13610234804785734989&#45;&gt;12886313978623292199 -->
<g id="edge9" class="edge">
<title>13610234804785734989&#45;&gt;12886313978623292199</title>
<path fill="none" stroke="#000000" d="M270.0044,-578.6209C306.8504,-571.0842 350.8074,-558.9783 387.0966,-540 405.807,-530.2149 406.8946,-522.5396 423.0966,-509 545.4715,-406.7344 554.4545,-343.9858 701.8132,-283 868.7716,-213.9026 1907.4953,-170.9362 2006.7834,-251 2065.5501,-298.3882 1987.4038,-365.6944 2042.7834,-417 2063.8641,-436.5299 2092.1154,-446.1618 2120.577,-450.2848"/>
<polygon fill="#000000" stroke="#000000" points="2120.2895,-453.7757 2130.6415,-451.5205 2121.1426,-446.8278 2120.2895,-453.7757"/>
</g>
<!-- 13610234804785734989&#45;&gt;12421771694198418854 -->
<g id="edge11" class="edge">
<title>13610234804785734989&#45;&gt;12421771694198418854</title>
<path fill="none" stroke="#000000" d="M292.2292,-590C331.0882,-590 376.2058,-590 417.4483,-590"/>
<polygon fill="#000000" stroke="#000000" points="417.5653,-593.5001 427.5652,-590 417.5652,-586.5001 417.5653,-593.5001"/>
</g>
<!-- 13610234804785734989&#45;&gt;11354074206287318022 -->
<g id="edge17" class="edge">
<title>13610234804785734989&#45;&gt;11354074206287318022</title>
<path fill="none" stroke="#000000" d="M269.2578,-578.3493C322.1913,-570.2035 393.2624,-559.2666 449.9191,-550.5478"/>
<polygon fill="#000000" stroke="#000000" points="450.6101,-553.9828 459.9614,-549.0025 449.5454,-547.0642 450.6101,-553.9828"/>
</g>
<!-- 13788869053623754394&#45;&gt;10848326615985732359 -->
<g id="edge23" class="edge">
<title>13788869053623754394&#45;&gt;10848326615985732359</title>
<path fill="none" stroke="#efefef" d="M2328.9261,-752.1279C2362.1469,-747.0005 2399.9056,-740.26 2433.8267,-732 2469.1564,-723.397 2507.9605,-710.8009 2538.427,-700.1608"/>
<polygon fill="#efefef" stroke="#efefef" points="2539.7432,-703.408 2548.0108,-696.7824 2537.416,-696.8061 2539.7432,-703.408"/>
</g>
<!-- 13788869053623754394&#45;&gt;10946161693179750605 -->
<g id="edge24" class="edge">
<title>13788869053623754394&#45;&gt;10946161693179750605</title>
<path fill="none" stroke="#efefef" d="M2257.9866,-745.9847C2293.5307,-713.3382 2370.6133,-642.0281 2433.8267,-580 2482.7238,-532.0198 2538.3468,-474.3901 2569.2372,-442.1042"/>
<polygon fill="#efefef" stroke="#efefef" points="2571.795,-444.4937 2576.1735,-434.846 2566.7342,-439.6575 2571.795,-444.4937"/>
</g>
<!-- 13788869053623754394&#45;&gt;11504827122213183863 -->
<g id="edge25" class="edge">
<title>13788869053623754394&#45;&gt;11504827122213183863</title>
<path fill="none" stroke="#efefef" d="M2279.7042,-780.9171C2344.9195,-807.5663 2471.4121,-859.2556 2542.1386,-888.1568"/>
<polygon fill="#efefef" stroke="#efefef" points="2541.1552,-891.5359 2551.7361,-892.0787 2543.8031,-885.056 2541.1552,-891.5359"/>
</g>
<!-- 13788869053623754394&#45;&gt;13151252664271832927 -->
<g id="edge26" class="edge">
<title>13788869053623754394&#45;&gt;13151252664271832927</title>
<path fill="none" stroke="#efefef" d="M2252.0278,-745.8405C2285.6557,-700.8597 2373.7991,-580.014 2433.8267,-471 2452.8291,-436.4905 2450.5693,-424.3679 2469.8267,-390 2501.7976,-332.9431 2547.0007,-271.1001 2572.8662,-237.1024"/>
<polygon fill="#efefef" stroke="#efefef" points="2575.7085,-239.1474 2579.0057,-229.0787 2570.1492,-234.8936 2575.7085,-239.1474"/>
</g>
<!-- 12886313978623292199&#45;&gt;10848326615985732359 -->
<g id="edge27" class="edge">
<title>12886313978623292199&#45;&gt;10848326615985732359</title>
<path fill="none" stroke="#efefef" d="M2303.3711,-459.1463C2343.1494,-470.3209 2393.8843,-488.1231 2433.8267,-514 2493.4165,-552.6056 2547.0636,-617.6297 2574.292,-653.7738"/>
<polygon fill="#efefef" stroke="#efefef" points="2571.545,-655.9452 2580.3207,-661.8813 2577.1623,-651.7682 2571.545,-655.9452"/>
</g>
<!-- 12886313978623292199&#45;&gt;10946161693179750605 -->
<g id="edge28" class="edge">
<title>12886313978623292199&#45;&gt;10946161693179750605</title>
<path fill="none" stroke="#efefef" d="M2344.2882,-435.9357C2385.3145,-432.814 2432.4586,-429.2267 2474.6035,-426.0199"/>
<polygon fill="#efefef" stroke="#efefef" points="2474.9319,-429.5051 2484.6375,-425.2564 2474.4007,-422.5253 2474.9319,-429.5051"/>
</g>
<!-- 12886313978623292199&#45;&gt;11504827122213183863 -->
<g id="edge29" class="edge">
<title>12886313978623292199&#45;&gt;11504827122213183863</title>
<path fill="none" stroke="#efefef" d="M2255.7066,-461.9557C2291.0337,-498.7849 2372.9669,-586.141 2433.8267,-666 2491.183,-741.2616 2550.0223,-836.694 2577.3777,-882.3313"/>
<polygon fill="#efefef" stroke="#efefef" points="2574.4408,-884.24 2582.5724,-891.0317 2580.4511,-880.6514 2574.4408,-884.24"/>
</g>
<!-- 12886313978623292199&#45;&gt;13151252664271832927 -->
<g id="edge30" class="edge">
<title>12886313978623292199&#45;&gt;13151252664271832927</title>
<path fill="none" stroke="#efefef" d="M2356.3744,-442.4994C2383.1611,-438.2304 2410.548,-430.4791 2433.8267,-417 2471.7517,-395.0403 2543.5999,-288.2079 2576.2452,-237.6383"/>
<polygon fill="#efefef" stroke="#efefef" points="2579.2259,-239.4739 2581.6856,-229.1685 2573.3363,-235.6907 2579.2259,-239.4739"/>
</g>
<!-- 12421771694198418854&#45;&gt;10848326615985732359 -->
<g id="edge31" class="edge">
<title>12421771694198418854&#45;&gt;10848326615985732359</title>
<path fill="none" stroke="#efefef" d="M551.6102,-608.0425C569.5714,-650.6861 620.9894,-757.6588 701.8132,-805 827.2051,-878.4463 1879.7912,-962.6431 2006.7834,-892 2033.2969,-877.2511 2017.1169,-850.178 2042.7834,-834 2190.6969,-740.7678 2270.1299,-852.433 2433.8267,-791 2485.977,-771.4288 2537.1792,-730.8792 2566.9314,-704.5854"/>
<polygon fill="#efefef" stroke="#efefef" points="2569.2732,-707.1865 2574.379,-697.9031 2564.5984,-701.9763 2569.2732,-707.1865"/>
</g>
<!-- 12421771694198418854&#45;&gt;10946161693179750605 -->
<g id="edge32" class="edge">
<title>12421771694198418854&#45;&gt;10946161693179750605</title>
<path fill="none" stroke="#efefef" d="M633.9341,-578.4054C645.2412,-574.6003 656.1991,-569.5894 665.8132,-563 689.6056,-546.6929 678.4122,-525.8639 701.8132,-509 816.9859,-426.0008 869.2793,-443.8331 1009.9879,-425 1174.2292,-403.0172 1217.1969,-419.9611 1382.8261,-425 1660.5258,-433.4484 1731.6638,-424.3004 2006.7834,-463 2023.0139,-465.2831 2026.491,-469.2115 2042.7834,-471 2215.5427,-489.9642 2262.3446,-499.2715 2433.8267,-471 2472.3458,-464.6495 2514.0767,-450.208 2545.1631,-437.8565"/>
<polygon fill="#efefef" stroke="#efefef" points="2546.4937,-441.0938 2554.4536,-434.1016 2543.8706,-434.6038 2546.4937,-441.0938"/>
</g>
<!-- 12421771694198418854&#45;&gt;11504827122213183863 -->
<g id="edge33" class="edge">
<title>12421771694198418854&#45;&gt;11504827122213183863</title>
<path fill="none" stroke="#efefef" d="M546.0388,-608.2079C552.4424,-669.8813 582.1468,-872.3648 701.8132,-968 876.1085,-1107.2936 973.2893,-1054 1196.407,-1054 1196.407,-1054 1196.407,-1054 1712.8048,-1054 2033.4365,-1054 2131.7006,-1137.3522 2433.8267,-1030 2489.106,-1010.358 2540.7949,-963.9611 2569.5669,-934.6652"/>
<polygon fill="#efefef" stroke="#efefef" points="2572.2939,-936.8796 2576.7175,-927.2524 2567.2558,-932.0197 2572.2939,-936.8796"/>
</g>
<!-- 12421771694198418854&#45;&gt;13151252664271832927 -->
<g id="edge34" class="edge">
<title>12421771694198418854&#45;&gt;13151252664271832927</title>
<path fill="none" stroke="#efefef" d="M641.6956,-579.8661C650.7034,-575.708 658.9918,-570.2145 665.8132,-563 733.5823,-491.3262 634.3142,-415.9281 701.8132,-344 797.9946,-241.5075 872.3798,-301.6283 1009.9879,-273 1190.9324,-235.356 1234.8286,-214.4028 1418.8261,-197 1694.9115,-170.8873 1765.4667,-194.6667 2042.7834,-194 2216.5799,-193.5822 2260.2751,-184.7669 2433.8267,-194 2451.7561,-194.9539 2470.7725,-196.5075 2489.1879,-198.2977"/>
<polygon fill="#efefef" stroke="#efefef" points="2489.0473,-201.801 2499.3462,-199.3143 2489.7444,-194.8358 2489.0473,-201.801"/>
</g>
<!-- 12421771694198418854&#45;&gt;10695730148961032308 -->
<g id="edge35" class="edge">
<title>12421771694198418854&#45;&gt;10695730148961032308</title>
<path fill="none" stroke="#efefef" d="M619.6434,-576.1638C658.8627,-568.9466 707.1103,-560.0681 748.2608,-552.4955"/>
<polygon fill="#efefef" stroke="#efefef" points="749.0845,-555.9028 758.2859,-550.6507 747.8176,-549.0184 749.0845,-555.9028"/>
</g>
<!-- 12421771694198418854&#45;&gt;13788869053623754394 -->
<g id="edge36" class="edge">
<title>12421771694198418854&#45;&gt;13788869053623754394</title>
<path fill="none" stroke="#efefef" d="M556.6437,-608.1649C586.2841,-651.6858 662.5739,-759.6731 701.8132,-778 833.1872,-839.3588 1861.8528,-782.3779 2006.7834,-778 2043.7582,-776.8831 2084.0091,-774.765 2120.4239,-772.5082"/>
<polygon fill="#efefef" stroke="#efefef" points="2120.8267,-775.9898 2130.5871,-771.8686 2120.387,-769.0036 2120.8267,-775.9898"/>
</g>
<!-- 12421771694198418854&#45;&gt;12886313978623292199 -->
<g id="edge37" class="edge">
<title>12421771694198418854&#45;&gt;12886313978623292199</title>
<path fill="none" stroke="#efefef" d="M640.7821,-579.8212C650.077,-575.6895 658.681,-570.2142 665.8132,-563 720.4169,-507.7692 641.503,-440.9354 701.8132,-392 814.4081,-300.6412 1868.4133,-348.6668 2006.7834,-392 2025.3728,-397.8216 2024.6369,-409.9176 2042.7834,-417 2066.0327,-426.0739 2091.8787,-432.1701 2116.9621,-436.2486"/>
<polygon fill="#efefef" stroke="#efefef" points="2116.4888,-439.7168 2126.9029,-437.7683 2117.5467,-432.7972 2116.4888,-439.7168"/>
</g>
<!-- 10129474377767673784&#45;&gt;10848326615985732359 -->
<g id="edge38" class="edge">
<title>10129474377767673784&#45;&gt;10848326615985732359</title>
<path fill="none" stroke="#efefef" d="M1951.348,-500.5553C2094.3385,-512.3703 2278.0424,-537.3329 2433.8267,-590 2480.5906,-605.8098 2529.2834,-635.5865 2560.3337,-656.5494"/>
<polygon fill="#efefef" stroke="#efefef" points="2558.3776,-659.4519 2568.6073,-662.2094 2562.33,-653.6744 2558.3776,-659.4519"/>
</g>
<!-- 10129474377767673784&#45;&gt;10946161693179750605 -->
<g id="edge39" class="edge">
<title>10129474377767673784&#45;&gt;10946161693179750605</title>
<path fill="none" stroke="#efefef" d="M1967.8413,-480.9415C1981.6834,-474.8245 1994.8049,-467.2677 2006.7834,-458 2042.4948,-430.3701 2005.6273,-388.6543 2042.7834,-363 2114.2929,-313.6266 2348.0856,-348.8643 2433.8267,-363 2472.3458,-369.3505 2514.0767,-383.792 2545.1631,-396.1435"/>
<polygon fill="#efefef" stroke="#efefef" points="2543.8706,-399.3962 2554.4536,-399.8984 2546.4937,-392.9062 2543.8706,-399.3962"/>
</g>
<!-- 10129474377767673784&#45;&gt;11504827122213183863 -->
<g id="edge40" class="edge">
<title>10129474377767673784&#45;&gt;11504827122213183863</title>
<path fill="none" stroke="#efefef" d="M1808.1158,-507.0611C1952.7033,-536.2277 2231.9746,-605.6606 2433.8267,-737 2494.9429,-776.7666 2548.5385,-845.1013 2575.2123,-882.5502"/>
<polygon fill="#efefef" stroke="#efefef" points="2572.4866,-884.7591 2581.0984,-890.9307 2578.2149,-880.7358 2572.4866,-884.7591"/>
</g>
<!-- 10129474377767673784&#45;&gt;13151252664271832927 -->
<g id="edge41" class="edge">
<title>10129474377767673784&#45;&gt;13151252664271832927</title>
<path fill="none" stroke="#efefef" d="M1744.5877,-472.0362C1802.3018,-440.0919 1928.9111,-373.0991 2042.7834,-334 2086.9525,-318.8341 2370.9913,-257.952 2512.8173,-227.9221"/>
<polygon fill="#efefef" stroke="#efefef" points="2513.719,-231.3088 2522.7777,-225.8143 2512.2697,-224.4605 2513.719,-231.3088"/>
</g>
<!-- 10129474377767673784&#45;&gt;10695730148961032308 -->
<g id="edge42" class="edge">
<title>10129474377767673784&#45;&gt;10695730148961032308</title>
<path fill="none" stroke="#efefef" d="M1460.5862,-499.3042C1326.6587,-504.7952 1159.3133,-512.5294 1009.9879,-522 994.5046,-522.982 978.2478,-524.1392 962.1632,-525.3606"/>
<polygon fill="#efefef" stroke="#efefef" points="961.8811,-521.8718 952.1789,-526.1284 962.4179,-528.8512 961.8811,-521.8718"/>
</g>
<!-- 10129474377767673784&#45;&gt;13788869053623754394 -->
<g id="edge43" class="edge">
<title>10129474377767673784&#45;&gt;13788869053623754394</title>
<path fill="none" stroke="#efefef" d="M1820.973,-506.7859C1882.4734,-521.427 1956.9236,-548.4733 2006.7834,-598 2039.1127,-630.1133 2010.3216,-662.0207 2042.7834,-694 2070.4866,-721.2915 2109.5538,-738.0119 2145.4913,-748.2242"/>
<polygon fill="#efefef" stroke="#efefef" points="2144.9658,-751.7076 2155.5325,-750.9354 2146.7906,-744.9496 2144.9658,-751.7076"/>
</g>
<!-- 10129474377767673784&#45;&gt;12886313978623292199 -->
<g id="edge44" class="edge">
<title>10129474377767673784&#45;&gt;12886313978623292199</title>
<path fill="none" stroke="#efefef" d="M1881.6067,-475.2238C1961.2734,-468.2501 2054.2104,-460.1148 2125.2442,-453.8969"/>
<polygon fill="#efefef" stroke="#efefef" points="2125.9048,-457.3525 2135.5615,-452.9937 2125.2944,-450.3792 2125.9048,-457.3525"/>
</g>
<!-- 11252927259328145570&#45;&gt;10848326615985732359 -->
<g id="edge45" class="edge">
<title>11252927259328145570&#45;&gt;10848326615985732359</title>
<path fill="none" stroke="#efefef" d="M1246.5728,-469.4056C1291.2465,-484.2426 1358.6582,-505.0146 1418.8261,-517 1863.883,-605.6548 1990.2053,-532.4212 2433.8267,-628 2469.8864,-635.7691 2509.3039,-648.5964 2539.9096,-659.5666"/>
<polygon fill="#efefef" stroke="#efefef" points="2538.9274,-662.9334 2549.5215,-663.0543 2541.3151,-656.3532 2538.9274,-662.9334"/>
</g>
<!-- 11252927259328145570&#45;&gt;10946161693179750605 -->
<g id="edge46" class="edge">
<title>11252927259328145570&#45;&gt;10946161693179750605</title>
<path fill="none" stroke="#efefef" d="M1284.07,-436.1046C1528.6325,-392.9546 2214.7824,-280.3636 2433.8267,-334 2480.5602,-345.4434 2528.8256,-373.2365 2559.8052,-393.5182"/>
<polygon fill="#efefef" stroke="#efefef" points="2558.1933,-396.6499 2568.456,-399.2818 2562.0745,-390.8244 2558.1933,-396.6499"/>
</g>
<!-- 11252927259328145570&#45;&gt;11504827122213183863 -->
<g id="edge47" class="edge">
<title>11252927259328145570&#45;&gt;11504827122213183863</title>
<path fill="none" stroke="#efefef" d="M1227.4141,-469.8522C1269.4755,-493.3418 1347.5606,-534.4021 1418.8261,-558 1671.6763,-641.7254 1808.9359,-495.6758 2006.7834,-674 2047.1964,-710.4251 2002.6707,-754.2444 2042.7834,-791 2105.667,-848.6207 2338.6872,-882.5258 2480.5814,-898.2664"/>
<polygon fill="#efefef" stroke="#efefef" points="2480.3148,-901.7581 2490.6367,-899.3684 2481.0775,-894.7997 2480.3148,-901.7581"/>
</g>
<!-- 11252927259328145570&#45;&gt;13151252664271832927 -->
<g id="edge48" class="edge">
<title>11252927259328145570&#45;&gt;13151252664271832927</title>
<path fill="none" stroke="#efefef" d="M1221.278,-434.1191C1260.9001,-406.673 1341.7656,-354.8407 1418.8261,-331 1444.0626,-323.1925 2190.8212,-250.0797 2480.1588,-221.9531"/>
<polygon fill="#efefef" stroke="#efefef" points="2480.6421,-225.4228 2490.2566,-220.9718 2479.9649,-218.4556 2480.6421,-225.4228"/>
</g>
<!-- 11252927259328145570&#45;&gt;10129474377767673784 -->
<g id="edge13" class="edge">
<title>11252927259328145570&#45;&gt;10129474377767673784</title>
<path fill="none" stroke="#000000" d="M1344.7318,-462.9147C1397.582,-466.8038 1457.9823,-471.2485 1514.3607,-475.3972"/>
<polygon fill="#000000" stroke="#000000" points="1514.2399,-478.8976 1524.4699,-476.1411 1514.7537,-471.9165 1514.2399,-478.8976"/>
</g>
<!-- 11354074206287318022&#45;&gt;10848326615985732359 -->
<g id="edge49" class="edge">
<title>11354074206287318022&#45;&gt;10848326615985732359</title>
<path fill="none" stroke="#efefef" d="M614.3393,-550.7438C705.1398,-569.443 868.7726,-601.5836 1009.9879,-621 1544.6686,-694.5161 2189.839,-689.9952 2461.9701,-683.8762"/>
<polygon fill="#efefef" stroke="#efefef" points="2462.3647,-687.3681 2472.2817,-683.6395 2462.204,-680.37 2462.3647,-687.3681"/>
</g>
<!-- 11354074206287318022&#45;&gt;10946161693179750605 -->
<g id="edge50" class="edge">
<title>11354074206287318022&#45;&gt;10946161693179750605</title>
<path fill="none" stroke="#efefef" d="M556.187,-517.8493C578.066,-483.9107 626.4444,-408.4415 665.8132,-344 682.225,-317.1362 675.3963,-300.1219 701.8132,-283 945.6313,-124.9719 1718.7,-164.2095 2006.7834,-202 2201.5582,-227.5504 2253.8953,-237.1667 2433.8267,-316 2481.2611,-336.7824 2530.8843,-370.4276 2561.8931,-393.115"/>
<polygon fill="#efefef" stroke="#efefef" points="2560.0065,-396.0729 2570.1282,-399.2037 2564.1681,-390.4443 2560.0065,-396.0729"/>
</g>
<!-- 11354074206287318022&#45;&gt;11504827122213183863 -->
<g id="edge51" class="edge">
<title>11354074206287318022&#45;&gt;11504827122213183863</title>
<path fill="none" stroke="#efefef" d="M642.4908,-546.6245C651.1749,-550.7163 659.1733,-556.0597 665.8132,-563 729.4589,-629.5258 637.8351,-700.7939 701.8132,-767 1121.4034,-1201.2024 1439.8105,-953.1985 2042.7834,-985 2216.3392,-994.1535 2264.316,-1023.3604 2433.8267,-985 2478.8497,-974.8113 2526.0483,-950.3002 2557.2745,-931.8489"/>
<polygon fill="#efefef" stroke="#efefef" points="2559.2698,-934.7332 2566.0374,-926.5815 2555.6634,-928.7337 2559.2698,-934.7332"/>
</g>
<!-- 11354074206287318022&#45;&gt;13151252664271832927 -->
<g id="edge52" class="edge">
<title>11354074206287318022&#45;&gt;13151252664271832927</title>
<path fill="none" stroke="#efefef" d="M546.6728,-517.6592C554.807,-459.2254 588.695,-276.5504 701.8132,-196 770.8517,-146.8384 886.6053,-137.2304 2042.7834,-102 2216.4998,-96.7066 2267.7925,-50.638 2433.8267,-102 2487.0229,-118.456 2538.279,-159.4666 2567.7047,-186.2263"/>
<polygon fill="#efefef" stroke="#efefef" points="2565.3399,-188.8066 2575.0562,-193.0311 2570.095,-183.6696 2565.3399,-188.8066"/>
</g>
<!-- 11354074206287318022&#45;&gt;10695730148961032308 -->
<g id="edge53" class="edge">
<title>11354074206287318022&#45;&gt;10695730148961032308</title>
<path fill="none" stroke="#efefef" d="M665.9287,-536C674.4283,-536 683.0444,-536 691.6584,-536"/>
<polygon fill="#efefef" stroke="#efefef" points="691.7623,-539.5001 701.7622,-536 691.7622,-532.5001 691.7623,-539.5001"/>
</g>
<!-- 11354074206287318022&#45;&gt;13788869053623754394 -->
<g id="edge54" class="edge">
<title>11354074206287318022&#45;&gt;13788869053623754394</title>
<path fill="none" stroke="#efefef" d="M642.3073,-546.8023C651.0268,-550.8598 659.0837,-556.1465 665.8132,-563 722.0571,-620.2806 640.0471,-688.7223 701.8132,-740 755.5479,-784.6101 1749.6295,-772.2939 2109.5699,-766.3357"/>
<polygon fill="#efefef" stroke="#efefef" points="2109.8327,-769.8319 2119.773,-766.1657 2109.716,-762.8329 2109.8327,-769.8319"/>
</g>
<!-- 11354074206287318022&#45;&gt;12886313978623292199 -->
<g id="edge55" class="edge">
<title>11354074206287318022&#45;&gt;12886313978623292199</title>
<path fill="none" stroke="#efefef" d="M553.2927,-517.7984C573.3327,-478.915 626.4115,-388.4843 701.8132,-354 965.535,-233.3892 1755.9652,-208.4445 2006.7834,-354 2034.6759,-370.1867 2016.0662,-398.9392 2042.7834,-417 2062.5314,-430.3497 2085.9838,-438.438 2109.6693,-443.1385"/>
<polygon fill="#efefef" stroke="#efefef" points="2109.365,-446.6394 2119.8244,-444.9511 2110.595,-439.7483 2109.365,-446.6394"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,519 @@
(function (__window) {
var exports = {};
Object.defineProperty(exports, '__esModule', { value: true });
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __read(o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
}
function __spread() {
for (var ar = [], i = 0; i < arguments.length; i++)
ar = ar.concat(__read(arguments[i]));
return ar;
}
/** An error emitted by Sentry SDKs and related utilities. */
var SentryError = /** @class */ (function (_super) {
__extends(SentryError, _super);
function SentryError(message) {
var _newTarget = this.constructor;
var _this = _super.call(this, message) || this;
_this.message = message;
// tslint:disable:no-unsafe-any
_this.name = _newTarget.prototype.constructor.name;
Object.setPrototypeOf(_this, _newTarget.prototype);
return _this;
}
return SentryError;
}(Error));
/**
* Checks whether given value's type is one of a few Error or Error-like
* {@link isError}.
*
* @param wat A value to be checked.
* @returns A boolean representing the result.
*/
/**
* Checks whether given value's type is an regexp
* {@link isRegExp}.
*
* @param wat A value to be checked.
* @returns A boolean representing the result.
*/
function isRegExp(wat) {
return Object.prototype.toString.call(wat) === '[object RegExp]';
}
/**
* Requires a module which is protected _against bundler minification.
*
* @param request The module path to resolve
*/
/**
* Checks whether we're in the Node.js or Browser environment
*
* @returns Answer to given question
*/
function isNodeEnv() {
// tslint:disable:strict-type-predicates
return Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]';
}
var fallbackGlobalObject = {};
/**
* Safely get global scope object
*
* @returns Global scope object
*/
function getGlobalObject() {
return (isNodeEnv()
? global
: typeof window !== 'undefined'
? window
: typeof self !== 'undefined'
? self
: fallbackGlobalObject);
}
/** JSDoc */
function consoleSandbox(callback) {
var global = getGlobalObject();
var levels = ['debug', 'info', 'warn', 'error', 'log', 'assert'];
if (!('console' in global)) {
return callback();
}
var originalConsole = global.console;
var wrappedLevels = {};
// Restore all wrapped console methods
levels.forEach(function (level) {
if (level in global.console && originalConsole[level].__sentry__) {
wrappedLevels[level] = originalConsole[level].__sentry_wrapped__;
originalConsole[level] = originalConsole[level].__sentry_original__;
}
});
// Perform callback manipulations
var result = callback();
// Revert restoration to wrapped state
Object.keys(wrappedLevels).forEach(function (level) {
originalConsole[level] = wrappedLevels[level];
});
return result;
}
// TODO: Implement different loggers for different environments
var global$1 = getGlobalObject();
/** Prefix for logging strings */
var PREFIX = 'Sentry Logger ';
/** JSDoc */
var Logger = /** @class */ (function () {
/** JSDoc */
function Logger() {
this._enabled = false;
}
/** JSDoc */
Logger.prototype.disable = function () {
this._enabled = false;
};
/** JSDoc */
Logger.prototype.enable = function () {
this._enabled = true;
};
/** JSDoc */
Logger.prototype.log = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
if (!this._enabled) {
return;
}
consoleSandbox(function () {
global$1.console.log(PREFIX + "[Log]: " + args.join(' ')); // tslint:disable-line:no-console
});
};
/** JSDoc */
Logger.prototype.warn = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
if (!this._enabled) {
return;
}
consoleSandbox(function () {
global$1.console.warn(PREFIX + "[Warn]: " + args.join(' ')); // tslint:disable-line:no-console
});
};
/** JSDoc */
Logger.prototype.error = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
if (!this._enabled) {
return;
}
consoleSandbox(function () {
global$1.console.error(PREFIX + "[Error]: " + args.join(' ')); // tslint:disable-line:no-console
});
};
return Logger;
}());
// Ensure we only have a single logger instance, even if multiple versions of @sentry/utils are being used
global$1.__SENTRY__ = global$1.__SENTRY__ || {};
var logger = global$1.__SENTRY__.logger || (global$1.__SENTRY__.logger = new Logger());
// tslint:disable:no-unsafe-any
/**
* Wrap a given object method with a higher-order function
*
* @param source An object that contains a method to be wrapped.
* @param name A name of method to be wrapped.
* @param replacement A function that should be used to wrap a given method.
* @returns void
*/
function fill(source, name, replacement) {
if (!(name in source)) {
return;
}
var original = source[name];
var wrapped = replacement(original);
// Make sure it's a function first, as we need to attach an empty prototype for `defineProperties` to work
// otherwise it'll throw "TypeError: Object.defineProperties called on non-object"
// tslint:disable-next-line:strict-type-predicates
if (typeof wrapped === 'function') {
try {
wrapped.prototype = wrapped.prototype || {};
Object.defineProperties(wrapped, {
__sentry__: {
enumerable: false,
value: true,
},
__sentry_original__: {
enumerable: false,
value: original,
},
__sentry_wrapped__: {
enumerable: false,
value: wrapped,
},
});
}
catch (_Oo) {
// This can throw if multiple fill happens on a global object like XMLHttpRequest
// Fixes https://github.com/getsentry/sentry-javascript/issues/2043
}
}
source[name] = wrapped;
}
// Slightly modified (no IE8 support, ES6) and transcribed to TypeScript
/**
* Checks if the value matches a regex or includes the string
* @param value The string value to be checked against
* @param pattern Either a regex or a string that must be contained in value
*/
function isMatchingPattern(value, pattern) {
if (isRegExp(pattern)) {
return pattern.test(value);
}
if (typeof pattern === 'string') {
return value.includes(pattern);
}
return false;
}
/**
* Tells whether current environment supports Fetch API
* {@link supportsFetch}.
*
* @returns Answer to the given question.
*/
function supportsFetch() {
if (!('fetch' in getGlobalObject())) {
return false;
}
try {
// tslint:disable-next-line:no-unused-expression
new Headers();
// tslint:disable-next-line:no-unused-expression
new Request('');
// tslint:disable-next-line:no-unused-expression
new Response();
return true;
}
catch (e) {
return false;
}
}
/**
* Tells whether current environment supports Fetch API natively
* {@link supportsNativeFetch}.
*
* @returns Answer to the given question.
*/
function supportsNativeFetch() {
if (!supportsFetch()) {
return false;
}
var global = getGlobalObject();
return global.fetch.toString().indexOf('native') !== -1;
}
/** SyncPromise internal states */
var States;
(function (States) {
/** Pending */
States["PENDING"] = "PENDING";
/** Resolved / OK */
States["RESOLVED"] = "RESOLVED";
/** Rejected / Error */
States["REJECTED"] = "REJECTED";
})(States || (States = {}));
/**
* Tracing Integration
*/
var Tracing = /** @class */ (function () {
/**
* Constructor for Tracing
*
* @param _options TracingOptions
*/
function Tracing(_options) {
if (_options === void 0) { _options = {}; }
this._options = _options;
/**
* @inheritDoc
*/
this.name = Tracing.id;
if (!Array.isArray(_options.tracingOrigins) || _options.tracingOrigins.length === 0) {
consoleSandbox(function () {
var defaultTracingOrigins = ['localhost', /^\//];
// @ts-ignore
console.warn('Sentry: You need to define `tracingOrigins` in the options. Set an array of urls or patterns to trace.');
// @ts-ignore
console.warn("Sentry: We added a reasonable default for you: " + defaultTracingOrigins);
_options.tracingOrigins = defaultTracingOrigins;
});
}
}
/**
* @inheritDoc
*/
Tracing.prototype.setupOnce = function (_, getCurrentHub) {
if (this._options.traceXHR !== false) {
this._traceXHR(getCurrentHub);
}
if (this._options.traceFetch !== false) {
this._traceFetch(getCurrentHub);
}
if (this._options.autoStartOnDomReady !== false) {
getGlobalObject().addEventListener('DOMContentLoaded', function () {
Tracing.startTrace(getCurrentHub(), getGlobalObject().location.href);
});
getGlobalObject().document.onreadystatechange = function () {
if (document.readyState === 'complete') {
Tracing.startTrace(getCurrentHub(), getGlobalObject().location.href);
}
};
}
};
/**
* Starts a new trace
* @param hub The hub to start the trace on
* @param transaction Optional transaction
*/
Tracing.startTrace = function (hub, transaction) {
hub.configureScope(function (scope) {
scope.startSpan();
scope.setTransaction(transaction);
});
};
/**
* JSDoc
*/
Tracing.prototype._traceXHR = function (getCurrentHub) {
if (!('XMLHttpRequest' in getGlobalObject())) {
return;
}
var xhrproto = XMLHttpRequest.prototype;
fill(xhrproto, 'open', function (originalOpen) {
return function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
// @ts-ignore
var self = getCurrentHub().getIntegration(Tracing);
if (self) {
self._xhrUrl = args[1];
}
// tslint:disable-next-line: no-unsafe-any
return originalOpen.apply(this, args);
};
});
fill(xhrproto, 'send', function (originalSend) {
return function () {
var _this = this;
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
// @ts-ignore
var self = getCurrentHub().getIntegration(Tracing);
if (self && self._xhrUrl && self._options.tracingOrigins) {
var url_1 = self._xhrUrl;
var headers_1 = getCurrentHub().traceHeaders();
// tslint:disable-next-line: prefer-for-of
var isWhitelisted = self._options.tracingOrigins.some(function (origin) {
return isMatchingPattern(url_1, origin);
});
if (isWhitelisted && this.setRequestHeader) {
Object.keys(headers_1).forEach(function (key) {
_this.setRequestHeader(key, headers_1[key]);
});
}
}
// tslint:disable-next-line: no-unsafe-any
return originalSend.apply(this, args);
};
});
};
/**
* JSDoc
*/
Tracing.prototype._traceFetch = function (getCurrentHub) {
if (!supportsNativeFetch()) {
return;
}
console.log("PATCHING FETCH");
// tslint:disable: only-arrow-functions
fill(getGlobalObject(), 'fetch', function (originalFetch) {
return function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
// @ts-ignore
var self = getCurrentHub().getIntegration(Tracing);
if (self && self._options.tracingOrigins) {
console.log("blafalseq");
var url_2 = args[0];
var options = args[1] = args[1] || {};
var whiteListed_1 = false;
self._options.tracingOrigins.forEach(function (whiteListUrl) {
if (!whiteListed_1) {
whiteListed_1 = isMatchingPattern(url_2, whiteListUrl);
console.log('a', url_2, whiteListUrl);
}
});
if (whiteListed_1) {
console.log('aaaaaa', options, whiteListed_1);
if (options.headers) {
if (Array.isArray(options.headers)) {
options.headers = __spread(options.headers, Object.entries(getCurrentHub().traceHeaders()));
}
else {
options.headers = __assign({}, options.headers, getCurrentHub().traceHeaders());
}
}
else {
options.headers = getCurrentHub().traceHeaders();
}
console.log(options.headers);
}
}
args[1] = options;
// tslint:disable-next-line: no-unsafe-any
return originalFetch.apply(getGlobalObject(), args);
};
});
// tslint:enable: only-arrow-functions
};
/**
* @inheritDoc
*/
Tracing.id = 'Tracing';
return Tracing;
}());
exports.Tracing = Tracing;
__window.Sentry = __window.Sentry || {};
__window.Sentry.Integrations = __window.Sentry.Integrations || {};
Object.assign(__window.Sentry.Integrations, exports);
}(window));
//# sourceMappingURL=tracing.js.map

View File

@ -0,0 +1,57 @@
<script src="https://browser.sentry-cdn.com/5.4.1/bundle.js" crossorigin="anonymous"></script>
<!-- TODO: Replace with real tracing integration once it's fixed -->
<script src="/static/tracing.js" crossorigin="anonymous"></script>
<script>
Sentry.init({
dsn: "{{ sentry_dsn }}",
integrations: [
new Sentry.Integrations.Tracing({ tracingOrigins: ['']})
],
debug: true
});
window.setTimeout(function() {
const scope = Sentry.getCurrentHub().getScope();
// TODO: Wait for Daniel's traceparent API
scope.setSpan(scope.getSpan().constructor.fromTraceparent(
"00-{{ traceparent['sentry-trace'].strip("-") }}-00"
));
});
async function compute() {
const res = await fetch(
"/compute/" +
document.getElementsByName('b64str')[0].value
);
const token = await res.text();
wait(token);
return false;
}
async function wait(token) {
const res = await fetch("/wait/" + token);
const line = await res.text();
document.getElementById('output').innerHTML += line;
document.getElementById('output').innerHTML += '<br>';
if(line == "NONE") {
window.setTimeout(function() { wait(token) }, 500);
}
}
</script>
<p>Decode your base64 string as a service (that calls another service)</p>
<input value='aGVsbG8gd29ybGQK' name=b64str> A base64 string<br>
<input type=button value=submit onclick="compute()">
<p>Output:</p>
<pre id=output />

View File

@ -0,0 +1,61 @@
import json
import sys
print("digraph mytrace {")
print("rankdir=LR")
all_spans = []
for line in sys.stdin:
event = json.loads(line)
if event.get("type") != "transaction":
continue
trace_ctx = event["contexts"]["trace"]
trace_span = dict(trace_ctx) # fake a span entry from transaction event
trace_span["description"] = event["transaction"]
trace_span["start_timestamp"] = event["start_timestamp"]
trace_span["timestamp"] = event["timestamp"]
if "parent_span_id" not in trace_ctx:
print(
'{} [label="trace:{} ({})"];'.format(
int(trace_ctx["trace_id"], 16),
event["transaction"],
trace_ctx["trace_id"],
)
)
for span in event["spans"] + [trace_span]:
print(
'{} [label="span:{} ({})"];'.format(
int(span["span_id"], 16), span["description"], span["span_id"]
)
)
if "parent_span_id" in span:
print(
"{} -> {};".format(
int(span["parent_span_id"], 16), int(span["span_id"], 16)
)
)
print(
"{} -> {} [style=dotted];".format(
int(span["trace_id"], 16), int(span["span_id"], 16)
)
)
all_spans.append(span)
for s1 in all_spans:
for s2 in all_spans:
if s1["start_timestamp"] > s2["timestamp"]:
print(
'{} -> {} [color="#efefef"];'.format(
int(s1["span_id"], 16), int(s2["span_id"], 16)
)
)
print("}")

View File

@ -0,0 +1,73 @@
import json
import flask
import os
import redis
import rq
import sentry_sdk
import time
import urllib3
from sentry_sdk.integrations.flask import FlaskIntegration
from sentry_sdk.integrations.rq import RqIntegration
app = flask.Flask(__name__)
redis_conn = redis.Redis()
http = urllib3.PoolManager()
queue = rq.Queue(connection=redis_conn)
def write_event(event):
with open("events", "a") as f:
f.write(json.dumps(event))
f.write("\n")
sentry_sdk.init(
integrations=[FlaskIntegration(), RqIntegration()],
traces_sample_rate=1.0,
traceparent_v2=True,
debug=True,
transport=write_event,
)
def decode_base64(encoded, redis_key):
time.sleep(1)
r = http.request("GET", "http://httpbin.org/base64/{}".format(encoded))
redis_conn.set(redis_key, r.data)
@app.route("/")
def index():
return flask.render_template(
"index.html",
sentry_dsn=os.environ["SENTRY_DSN"],
traceparent=dict(sentry_sdk.Hub.current.iter_trace_propagation_headers()),
)
@app.route("/compute/<input>")
def compute(input):
redis_key = "sentry-python-tracing-example-result:{}".format(input)
redis_conn.delete(redis_key)
queue.enqueue(decode_base64, encoded=input, redis_key=redis_key)
return redis_key
@app.route("/wait/<redis_key>")
def wait(redis_key):
result = redis_conn.get(redis_key)
if result is None:
return "NONE"
else:
redis_conn.delete(redis_key)
return "RESULT: {}".format(result)
@app.cli.command("worker")
def run_worker():
print("WORKING")
worker = rq.Worker([queue], connection=queue.connection)
worker.work()

60
mypy.ini Normal file
View File

@ -0,0 +1,60 @@
[mypy]
allow_redefinition = True
check_untyped_defs = True
; disallow_any_decorated = True
; disallow_any_explicit = True
; disallow_any_expr = True
disallow_any_generics = True
; disallow_any_unimported = True
disallow_incomplete_defs = True
; disallow_subclassing_any = True
; disallow_untyped_calls = True
disallow_untyped_decorators = True
disallow_untyped_defs = True
no_implicit_optional = True
strict_equality = True
strict_optional = True
warn_redundant_casts = True
; warn_return_any = True
; warn_unused_configs = True
; warn_unused_ignores = True
; Relaxations:
[mypy-sentry_sdk._compat]
disallow_untyped_defs = False
[mypy-sentry_sdk.scope]
disallow_untyped_defs = False
[mypy-sentry_sdk.integrations.*]
disallow_any_generics = False
disallow_untyped_defs = False
[mypy-sentry_sdk.integrations.aiohttp]
disallow_any_generics = True
disallow_untyped_defs = True
[mypy-sentry_sdk.utils]
disallow_any_generics = False
disallow_untyped_defs = False
[mypy-django.*]
ignore_missing_imports = True
[mypy-pyramid.*]
ignore_missing_imports = True
[mypy-psycopg2.*]
ignore_missing_imports = True
[mypy-pytest.*]
ignore_missing_imports = True
[mypy-aiohttp.*]
ignore_missing_imports = True
[mypy-sanic.*]
ignore_missing_imports = True
[mypy-tornado.*]
ignore_missing_imports = True
[mypy-fakeredis.*]
ignore_missing_imports = True
[mypy-rq.*]
ignore_missing_imports = True

4
pytest.ini Normal file
View File

@ -0,0 +1,4 @@
[pytest]
DJANGO_SETTINGS_MODULE = tests.integrations.django.myapp.settings
addopts = --boxed --tb=short
markers = tests_internal_exceptions

11
scripts/aws-cleanup.sh Normal file
View File

@ -0,0 +1,11 @@
#!/bin/sh
# Delete all AWS Lambda functions
export AWS_ACCESS_KEY_ID="$SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID"
export AWS_SECRET_ACCESS_KEY="$SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY"
export AWS_IAM_ROLE="$SENTRY_PYTHON_TEST_AWS_IAM_ROLE"
for func in $(aws lambda list-functions | jq -r .Functions[].FunctionName); do
echo "Deleting $func"
aws lambda delete-function --function-name $func
done

21
scripts/bump-version.sh Executable file
View File

@ -0,0 +1,21 @@
#!/bin/bash
set -eux
SCRIPT_DIR="$( dirname "$0" )"
cd $SCRIPT_DIR/..
OLD_VERSION="${1}"
NEW_VERSION="${2}"
echo "Current version: $OLD_VERSION"
echo "Bumping version: $NEW_VERSION"
function replace() {
! grep "$2" $3
perl -i -pe "s/$1/$2/g" $3
grep "$2" $3 # verify that replacement was successful
}
replace "version=\"[0-9.]+\"" "version=\"$NEW_VERSION\"" ./setup.py
replace "VERSION = \"[0-9.]+\"" "VERSION = \"$NEW_VERSION\"" ./sentry_sdk/consts.py
replace "release = \"[0-9.]+\"" "release = \"$NEW_VERSION\"" ./docs/conf.py

31
scripts/download-semaphore.sh Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
set -e
if { [ "$TRAVIS" == "true" ] || [ "$TF_BUILD" == "True" ]; } && [ -z "$GITHUB_API_TOKEN" ]; then
echo "Not running on external pull request"
exit 0;
fi
target=semaphore
# Download the latest semaphore release for Travis
output="$(
curl -s \
https://api.github.com/repos/getsentry/semaphore/releases/latest?access_token=$GITHUB_API_TOKEN
)"
echo "$output"
output="$(echo "$output" \
| grep "$(uname -s)" \
| grep -v "\.zip" \
| grep "download" \
| cut -d : -f 2,3 \
| tr -d , \
| tr -d \")"
echo "$output"
echo "$output" | wget -i - -O $target
[ -s $target ]
chmod +x $target

23
scripts/runtox.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/bash
set -ex
if [ -n "$TOXPATH" ]; then
true
elif which tox &> /dev/null; then
TOXPATH=tox
else
TOXPATH=./.venv/bin/tox
fi
# Usage: sh scripts/runtox.sh py3.7 <pytest-args>
# Runs all environments with substring py3.7 and the given arguments for pytest
if [ -n "$1" ]; then
searchstring="$1"
elif [ -n "$TRAVIS_PYTHON_VERSION" ]; then
searchstring="$(echo py$TRAVIS_PYTHON_VERSION | sed -e 's/pypypy/pypy/g' -e 's/-dev//g')"
elif [ -n "$AZURE_PYTHON_VERSION" ]; then
searchstring="$(echo py$AZURE_PYTHON_VERSION | sed -e 's/pypypy/pypy/g' -e 's/-dev//g')"
fi
exec $TOXPATH -e $($TOXPATH -l | grep "$searchstring" | tr '\n' ',') -- "${@:2}"

25
sentry_sdk/__init__.py Normal file
View File

@ -0,0 +1,25 @@
from sentry_sdk.hub import Hub, init
from sentry_sdk.scope import Scope
from sentry_sdk.transport import Transport, HttpTransport
from sentry_sdk.client import Client
from sentry_sdk.api import * # noqa
from sentry_sdk.api import __all__ as api_all
from sentry_sdk.consts import VERSION # noqa
__all__ = api_all + [ # noqa
"Hub",
"Scope",
"Client",
"Transport",
"HttpTransport",
"init",
"integrations",
]
# Initialize the debug support after everything is loaded
from sentry_sdk.debug import init_debug_support
init_debug_support()
del init_debug_support

87
sentry_sdk/_compat.py Normal file
View File

@ -0,0 +1,87 @@
import sys
from sentry_sdk._types import MYPY
if MYPY:
from typing import Optional
from typing import Tuple
from typing import Any
from typing import Type
PY2 = sys.version_info[0] == 2
if PY2:
import urlparse # noqa
text_type = unicode # noqa
import Queue as queue # noqa
string_types = (str, text_type)
number_types = (int, long, float) # noqa
int_types = (int, long) # noqa
iteritems = lambda x: x.iteritems() # noqa: B301
def implements_str(cls):
cls.__unicode__ = cls.__str__
cls.__str__ = lambda x: unicode(x).encode("utf-8") # noqa
return cls
exec("def reraise(tp, value, tb=None):\n raise tp, value, tb")
else:
import urllib.parse as urlparse # noqa
import queue # noqa
text_type = str
string_types = (text_type,) # type: Tuple[type]
number_types = (int, float) # type: Tuple[type, type]
int_types = (int,) # noqa
iteritems = lambda x: x.items()
def _identity(x):
return x
def implements_str(x):
return x
def reraise(tp, value, tb=None):
# type: (Optional[Type[BaseException]], Optional[BaseException], Optional[Any]) -> None
assert value is not None
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
def with_metaclass(meta, *bases):
class metaclass(type):
def __new__(cls, name, this_bases, d):
return meta(name, bases, d)
return type.__new__(metaclass, "temporary_class", (), {})
def check_thread_support():
# type: () -> None
try:
from uwsgi import opt # type: ignore
except ImportError:
return
# When `threads` is passed in as a uwsgi option,
# `enable-threads` is implied on.
if "threads" in opt:
return
if str(opt.get("enable-threads", "0")).lower() in ("false", "off", "no", "0"):
from warnings import warn
warn(
Warning(
"We detected the use of uwsgi with disabled threads. "
"This will cause issues with the transport you are "
"trying to use. Please enable threading for uwsgi. "
'(Enable the "enable-threads" flag).'
)
)

28
sentry_sdk/_types.py Normal file
View File

@ -0,0 +1,28 @@
try:
from typing import TYPE_CHECKING as MYPY
except ImportError:
MYPY = False
if MYPY:
from types import TracebackType
from typing import Any
from typing import Callable
from typing import Dict
from typing import Optional
from typing import Tuple
from typing import Type
ExcInfo = Tuple[
Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]
]
Event = Dict[str, Any]
Hint = Dict[str, Any]
Breadcrumb = Dict[str, Any]
BreadcrumbHint = Dict[str, Any]
EventProcessor = Callable[[Event, Hint], Optional[Event]]
ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]]
BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]]

195
sentry_sdk/api.py Normal file
View File

@ -0,0 +1,195 @@
import inspect
from contextlib import contextmanager
from sentry_sdk.hub import Hub
from sentry_sdk.scope import Scope
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
from typing import Optional
from typing import overload
from typing import Callable
from typing import TypeVar
from typing import ContextManager
from sentry_sdk._types import Event, Hint, Breadcrumb, BreadcrumbHint
from sentry_sdk.tracing import Span
T = TypeVar("T")
F = TypeVar("F", bound=Callable[..., Any])
else:
def overload(x):
# type: (T) -> T
return x
__all__ = [
"capture_event",
"capture_message",
"capture_exception",
"add_breadcrumb",
"configure_scope",
"push_scope",
"flush",
"last_event_id",
"start_span",
]
def hubmethod(f):
# type: (F) -> F
f.__doc__ = "%s\n\n%s" % (
"Alias for :py:meth:`sentry_sdk.Hub.%s`" % f.__name__,
inspect.getdoc(getattr(Hub, f.__name__)),
)
return f
@hubmethod
def capture_event(
event, # type: Event
hint=None, # type: Optional[Hint]
):
# type: (...) -> Optional[str]
hub = Hub.current
if hub is not None:
return hub.capture_event(event, hint)
return None
@hubmethod
def capture_message(
message, # type: str
level=None, # type: Optional[str]
):
# type: (...) -> Optional[str]
hub = Hub.current
if hub is not None:
return hub.capture_message(message, level)
return None
@hubmethod
def capture_exception(
error=None # type: Optional[BaseException]
):
# type: (...) -> Optional[str]
hub = Hub.current
if hub is not None:
return hub.capture_exception(error)
return None
@hubmethod
def add_breadcrumb(
crumb=None, # type: Optional[Breadcrumb]
hint=None, # type: Optional[BreadcrumbHint]
**kwargs # type: Any
):
# type: (...) -> None
hub = Hub.current
if hub is not None:
return hub.add_breadcrumb(crumb, hint, **kwargs)
@overload # noqa
def configure_scope():
# type: () -> ContextManager[Scope]
pass
@overload # noqa
def configure_scope(
callback # type: Callable[[Scope], None]
):
# type: (...) -> None
pass
@hubmethod # noqa
def configure_scope(
callback=None # type: Optional[Callable[[Scope], None]]
):
# type: (...) -> Optional[ContextManager[Scope]]
hub = Hub.current
if hub is not None:
return hub.configure_scope(callback)
elif callback is None:
@contextmanager
def inner():
yield Scope()
return inner()
else:
# returned if user provided callback
return None
@overload # noqa
def push_scope():
# type: () -> ContextManager[Scope]
pass
@overload # noqa
def push_scope(
callback # type: Callable[[Scope], None]
):
# type: (...) -> None
pass
@hubmethod # noqa
def push_scope(
callback=None # type: Optional[Callable[[Scope], None]]
):
# type: (...) -> Optional[ContextManager[Scope]]
hub = Hub.current
if hub is not None:
return hub.push_scope(callback)
elif callback is None:
@contextmanager
def inner():
yield Scope()
return inner()
else:
# returned if user provided callback
return None
@hubmethod
def flush(
timeout=None, # type: Optional[float]
callback=None, # type: Optional[Callable[[int, float], None]]
):
# type: (...) -> None
hub = Hub.current
if hub is not None:
return hub.flush(timeout=timeout, callback=callback)
@hubmethod
def last_event_id():
# type: () -> Optional[str]
hub = Hub.current
if hub is not None:
return hub.last_event_id()
return None
@hubmethod
def start_span(
span=None, # type: Optional[Span]
**kwargs # type: Any
):
# type: (...) -> Span
# TODO: All other functions in this module check for
# `Hub.current is None`. That actually should never happen?
return Hub.current.start_span(span=span, **kwargs)

333
sentry_sdk/client.py Normal file
View File

@ -0,0 +1,333 @@
import os
import uuid
import random
from datetime import datetime
import socket
from sentry_sdk._compat import string_types, text_type, iteritems
from sentry_sdk.utils import (
handle_in_app,
get_type_name,
capture_internal_exceptions,
current_stacktrace,
logger,
)
from sentry_sdk.serializer import Serializer
from sentry_sdk.transport import make_transport
from sentry_sdk.consts import DEFAULT_OPTIONS, SDK_INFO, ClientConstructor
from sentry_sdk.integrations import setup_integrations
from sentry_sdk.utils import ContextVar
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
from typing import Callable
from typing import Dict
from typing import Optional
from sentry_sdk.scope import Scope
from sentry_sdk._types import Event, Hint
_client_init_debug = ContextVar("client_init_debug")
_client_in_capture_event = ContextVar("client_in_capture_event")
def _get_options(*args, **kwargs):
# type: (*Optional[str], **Any) -> Dict[str, Any]
if args and (isinstance(args[0], (text_type, bytes, str)) or args[0] is None):
dsn = args[0] # type: Optional[str]
args = args[1:]
else:
dsn = None
rv = dict(DEFAULT_OPTIONS)
options = dict(*args, **kwargs) # type: ignore
if dsn is not None and options.get("dsn") is None:
options["dsn"] = dsn # type: ignore
for key, value in iteritems(options):
if key not in rv:
raise TypeError("Unknown option %r" % (key,))
rv[key] = value
if rv["dsn"] is None:
rv["dsn"] = os.environ.get("SENTRY_DSN")
if rv["release"] is None:
rv["release"] = os.environ.get("SENTRY_RELEASE")
if rv["environment"] is None:
rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT")
if rv["server_name"] is None and hasattr(socket, "gethostname"):
rv["server_name"] = socket.gethostname()
return rv # type: ignore
class _Client(object):
"""The client is internally responsible for capturing the events and
forwarding them to sentry through the configured transport. It takes
the client options as keyword arguments and optionally the DSN as first
argument.
"""
def __init__(self, *args, **kwargs):
# type: (*Any, **Any) -> None
self.options = get_options(*args, **kwargs) # type: Dict[str, Any]
self._init_impl()
def __getstate__(self):
# type: () -> Any
return {"options": self.options}
def __setstate__(self, state):
# type: (Any) -> None
self.options = state["options"]
self._init_impl()
def _init_impl(self):
# type: () -> None
old_debug = _client_init_debug.get(False)
try:
_client_init_debug.set(self.options["debug"])
self.transport = make_transport(self.options)
request_bodies = ("always", "never", "small", "medium")
if self.options["request_bodies"] not in request_bodies:
raise ValueError(
"Invalid value for request_bodies. Must be one of {}".format(
request_bodies
)
)
self.integrations = setup_integrations(
self.options["integrations"],
with_defaults=self.options["default_integrations"],
)
finally:
_client_init_debug.set(old_debug)
@property
def dsn(self):
# type: () -> Optional[str]
"""Returns the configured DSN as string."""
return self.options["dsn"]
def _prepare_event(
self,
event, # type: Event
hint, # type: Optional[Hint]
scope, # type: Optional[Scope]
):
# type: (...) -> Optional[Event]
if event.get("timestamp") is None:
event["timestamp"] = datetime.utcnow()
hint = dict(hint or ()) # type: Hint
if scope is not None:
event_ = scope.apply_to_event(event, hint)
if event_ is None:
return None
event = event_
if (
self.options["attach_stacktrace"]
and "exception" not in event
and "stacktrace" not in event
and "threads" not in event
):
with capture_internal_exceptions():
event["threads"] = {
"values": [
{
"stacktrace": current_stacktrace(
self.options["with_locals"]
),
"crashed": False,
"current": True,
}
]
}
for key in "release", "environment", "server_name", "dist":
if event.get(key) is None and self.options[key] is not None: # type: ignore
event[key] = text_type(self.options[key]).strip() # type: ignore
if event.get("sdk") is None:
sdk_info = dict(SDK_INFO)
sdk_info["integrations"] = sorted(self.integrations.keys())
event["sdk"] = sdk_info
if event.get("platform") is None:
event["platform"] = "python"
event = handle_in_app(
event, self.options["in_app_exclude"], self.options["in_app_include"]
)
# Postprocess the event here so that annotated types do
# generally not surface in before_send
if event is not None:
event = Serializer().serialize_event(event)
before_send = self.options["before_send"]
if before_send is not None:
new_event = None
with capture_internal_exceptions():
new_event = before_send(event, hint or {})
if new_event is None:
logger.info("before send dropped event (%s)", event)
event = new_event # type: ignore
return event
def _is_ignored_error(self, event, hint):
# type: (Event, Hint) -> bool
exc_info = hint.get("exc_info")
if exc_info is None:
return False
type_name = get_type_name(exc_info[0])
full_name = "%s.%s" % (exc_info[0].__module__, type_name)
for errcls in self.options["ignore_errors"]:
# String types are matched against the type name in the
# exception only
if isinstance(errcls, string_types):
if errcls == full_name or errcls == type_name:
return True
else:
if issubclass(exc_info[0], errcls): # type: ignore
return True
return False
def _should_capture(
self,
event, # type: Event
hint, # type: Hint
scope=None, # type: Optional[Scope]
):
# type: (...) -> bool
if scope is not None and not scope._should_capture:
return False
if (
self.options["sample_rate"] < 1.0
and random.random() >= self.options["sample_rate"]
):
return False
if self._is_ignored_error(event, hint):
return False
return True
def capture_event(
self,
event, # type: Event
hint=None, # type: Optional[Hint]
scope=None, # type: Optional[Scope]
):
# type: (...) -> Optional[str]
"""Captures an event.
:param event: A ready-made event that can be directly sent to Sentry.
:param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object.
:returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help.
"""
is_recursive = _client_in_capture_event.get(False)
if is_recursive:
return None
_client_in_capture_event.set(True)
try:
if self.transport is None:
return None
if hint is None:
hint = {}
event_id = event.get("event_id")
if event_id is None:
event["event_id"] = event_id = uuid.uuid4().hex
if not self._should_capture(event, hint, scope):
return None
event_opt = self._prepare_event(event, hint, scope)
if event_opt is None:
return None
self.transport.capture_event(event_opt)
return event_id
finally:
_client_in_capture_event.set(False)
def close(
self,
timeout=None, # type: Optional[float]
callback=None, # type: Optional[Callable[[int, float], None]]
):
# type: (...) -> None
"""
Close the client and shut down the transport. Arguments have the same
semantics as :py:meth:`Client.flush`.
"""
if self.transport is not None:
self.flush(timeout=timeout, callback=callback)
self.transport.kill()
self.transport = None
def flush(
self,
timeout=None, # type: Optional[float]
callback=None, # type: Optional[Callable[[int, float], None]]
):
# type: (...) -> None
"""
Wait for the current events to be sent.
:param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used.
:param callback: Is invoked with the number of pending events and the configured timeout.
"""
if self.transport is not None:
if timeout is None:
timeout = self.options["shutdown_timeout"]
self.transport.flush(timeout=timeout, callback=callback)
def __enter__(self):
# type: () -> _Client
return self
def __exit__(self, exc_type, exc_value, tb):
# type: (Any, Any, Any) -> None
self.close()
from sentry_sdk._types import MYPY
if MYPY:
# Make mypy, PyCharm and other static analyzers think `get_options` is a
# type to have nicer autocompletion for params.
#
# Use `ClientConstructor` to define the argument types of `init` and
# `Dict[str, Any]` to tell static analyzers about the return type.
class get_options(ClientConstructor, Dict[str, Any]):
pass
class Client(ClientConstructor, _Client):
pass
else:
# Alias `get_options` for actual usage. Go through the lambda indirection
# to throw PyCharm off of the weakly typed signature (it would otherwise
# discover both the weakly typed signature of `_init` and our faked `init`
# type).
get_options = (lambda: _get_options)()
Client = (lambda: _Client)()

80
sentry_sdk/consts.py Normal file
View File

@ -0,0 +1,80 @@
from sentry_sdk._types import MYPY
if MYPY:
from typing import Optional
from typing import Callable
from typing import Union
from typing import List
from typing import Type
from typing import Dict
from typing import Any
from typing import Sequence
from sentry_sdk.transport import Transport
from sentry_sdk.integrations import Integration
from sentry_sdk._types import Event, EventProcessor, BreadcrumbProcessor
# This type exists to trick mypy and PyCharm into thinking `init` and `Client`
# take these arguments (even though they take opaque **kwargs)
class ClientConstructor(object):
def __init__(
self,
dsn=None, # type: Optional[str]
with_locals=True, # type: bool
max_breadcrumbs=100, # type: int
release=None, # type: Optional[str]
environment=None, # type: Optional[str]
server_name=None, # type: Optional[str]
shutdown_timeout=2, # type: int
integrations=[], # type: Sequence[Integration]
in_app_include=[], # type: List[str]
in_app_exclude=[], # type: List[str]
default_integrations=True, # type: bool
dist=None, # type: Optional[str]
transport=None, # type: Optional[Union[Transport, Type[Transport], Callable[[Event], None]]]
sample_rate=1.0, # type: float
send_default_pii=False, # type: bool
http_proxy=None, # type: Optional[str]
https_proxy=None, # type: Optional[str]
ignore_errors=[], # type: List[Union[type, str]]
request_bodies="medium", # type: str
before_send=None, # type: Optional[EventProcessor]
before_breadcrumb=None, # type: Optional[BreadcrumbProcessor]
debug=False, # type: bool
attach_stacktrace=False, # type: bool
ca_certs=None, # type: Optional[str]
propagate_traces=True, # type: bool
# DO NOT ENABLE THIS RIGHT NOW UNLESS YOU WANT TO EXCEED YOUR EVENT QUOTA IMMEDIATELY
traces_sample_rate=0.0, # type: float
traceparent_v2=False, # type: bool
_experiments={}, # type: Dict[str, Any]
):
# type: (...) -> None
pass
def _get_default_options():
# type: () -> Dict[str, Any]
import inspect
if hasattr(inspect, "getfullargspec"):
getargspec = inspect.getfullargspec # type: ignore
else:
getargspec = inspect.getargspec # type: ignore
a = getargspec(ClientConstructor.__init__)
return dict(zip(a.args[-len(a.defaults) :], a.defaults))
DEFAULT_OPTIONS = _get_default_options()
del _get_default_options
VERSION = "0.12.2"
SDK_INFO = {
"name": "sentry.python",
"version": VERSION,
"packages": [{"name": "pypi:sentry-sdk", "version": VERSION}],
}

44
sentry_sdk/debug.py Normal file
View File

@ -0,0 +1,44 @@
import sys
import logging
from sentry_sdk import utils
from sentry_sdk.hub import Hub
from sentry_sdk.utils import logger
from sentry_sdk.client import _client_init_debug
from logging import LogRecord
class _HubBasedClientFilter(logging.Filter):
def filter(self, record):
# type: (LogRecord) -> bool
if _client_init_debug.get(False):
return True
hub = Hub.current
if hub is not None and hub.client is not None:
return hub.client.options["debug"]
return False
def init_debug_support():
# type: () -> None
if not logger.handlers:
configure_logger()
configure_debug_hub()
def configure_logger():
# type: () -> None
_handler = logging.StreamHandler(sys.stderr)
_handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s"))
logger.addHandler(_handler)
logger.setLevel(logging.DEBUG)
logger.addFilter(_HubBasedClientFilter())
def configure_debug_hub():
# type: () -> None
def _get_debug_hub():
# type: () -> Hub
return Hub.current
utils._get_debug_hub = _get_debug_hub

589
sentry_sdk/hub.py Normal file
View File

@ -0,0 +1,589 @@
import copy
import random
import sys
import weakref
from datetime import datetime
from contextlib import contextmanager
from warnings import warn
from sentry_sdk._compat import with_metaclass
from sentry_sdk.scope import Scope
from sentry_sdk.client import Client
from sentry_sdk.tracing import Span
from sentry_sdk.utils import (
exc_info_from_error,
event_from_exception,
logger,
ContextVar,
)
from sentry_sdk._types import MYPY
if MYPY:
from typing import Union
from typing import Any
from typing import Optional
from typing import Tuple
from typing import List
from typing import Callable
from typing import Generator
from typing import Type
from typing import TypeVar
from typing import overload
from typing import ContextManager
from sentry_sdk.integrations import Integration
from sentry_sdk._types import Event, Hint, Breadcrumb, BreadcrumbHint, ExcInfo
from sentry_sdk.consts import ClientConstructor
T = TypeVar("T")
else:
def overload(x):
# type: (T) -> T
return x
_local = ContextVar("sentry_current_hub") # type: ignore
_initial_client = None # type: Optional[weakref.ReferenceType[Client]]
def _should_send_default_pii():
# type: () -> bool
client = Hub.current.client
if not client:
return False
return client.options["send_default_pii"]
class _InitGuard(object):
def __init__(self, client):
# type: (Client) -> None
self._client = client
def __enter__(self):
# type: () -> _InitGuard
return self
def __exit__(self, exc_type, exc_value, tb):
# type: (Any, Any, Any) -> None
c = self._client
if c is not None:
c.close()
def _init(*args, **kwargs):
# type: (*Optional[str], **Any) -> ContextManager[Any]
"""Initializes the SDK and optionally integrations.
This takes the same arguments as the client constructor.
"""
global _initial_client
client = Client(*args, **kwargs) # type: ignore
Hub.current.bind_client(client)
rv = _InitGuard(client)
if client is not None:
_initial_client = weakref.ref(client)
return rv
from sentry_sdk._types import MYPY
if MYPY:
# Make mypy, PyCharm and other static analyzers think `init` is a type to
# have nicer autocompletion for params.
#
# Use `ClientConstructor` to define the argument types of `init` and
# `ContextManager[Any]` to tell static analyzers about the return type.
class init(ClientConstructor, ContextManager[Any]):
pass
else:
# Alias `init` for actual usage. Go through the lambda indirection to throw
# PyCharm off of the weakly typed signature (it would otherwise discover
# both the weakly typed signature of `_init` and our faked `init` type).
init = (lambda: _init)()
class HubMeta(type):
@property
def current(self):
# type: () -> Hub
"""Returns the current instance of the hub."""
rv = _local.get(None)
if rv is None:
rv = Hub(GLOBAL_HUB)
_local.set(rv)
return rv
@property
def main(self):
# type: () -> Hub
"""Returns the main instance of the hub."""
return GLOBAL_HUB
class _ScopeManager(object):
def __init__(self, hub):
# type: (Hub) -> None
self._hub = hub
self._original_len = len(hub._stack)
self._layer = hub._stack[-1]
def __enter__(self):
# type: () -> Scope
scope = self._layer[1]
assert scope is not None
return scope
def __exit__(self, exc_type, exc_value, tb):
# type: (Any, Any, Any) -> None
current_len = len(self._hub._stack)
if current_len < self._original_len:
logger.error(
"Scope popped too soon. Popped %s scopes too many.",
self._original_len - current_len,
)
return
elif current_len > self._original_len:
logger.warning(
"Leaked %s scopes: %s",
current_len - self._original_len,
self._hub._stack[self._original_len :],
)
layer = self._hub._stack[self._original_len - 1]
del self._hub._stack[self._original_len - 1 :]
if layer[1] != self._layer[1]:
logger.error(
"Wrong scope found. Meant to pop %s, but popped %s.",
layer[1],
self._layer[1],
)
elif layer[0] != self._layer[0]:
warning = (
"init() called inside of pushed scope. This might be entirely "
"legitimate but usually occurs when initializing the SDK inside "
"a request handler or task/job function. Try to initialize the "
"SDK as early as possible instead."
)
logger.warning(warning)
class Hub(with_metaclass(HubMeta)): # type: ignore
"""The hub wraps the concurrency management of the SDK. Each thread has
its own hub but the hub might transfer with the flow of execution if
context vars are available.
If the hub is used with a with statement it's temporarily activated.
"""
_stack = None # type: List[Tuple[Optional[Client], Scope]]
# Mypy doesn't pick up on the metaclass.
if MYPY:
current = None # type: Hub
main = None # type: Hub
def __init__(
self,
client_or_hub=None, # type: Optional[Union[Hub, Client]]
scope=None, # type: Optional[Any]
):
# type: (...) -> None
if isinstance(client_or_hub, Hub):
hub = client_or_hub
client, other_scope = hub._stack[-1]
if scope is None:
scope = copy.copy(other_scope)
else:
client = client_or_hub
if scope is None:
scope = Scope()
self._stack = [(client, scope)]
self._last_event_id = None # type: Optional[str]
self._old_hubs = [] # type: List[Hub]
def __enter__(self):
# type: () -> Hub
self._old_hubs.append(Hub.current)
_local.set(self)
return self
def __exit__(
self,
exc_type, # type: Optional[type]
exc_value, # type: Optional[BaseException]
tb, # type: Optional[Any]
):
# type: (...) -> None
old = self._old_hubs.pop()
_local.set(old)
def run(
self, callback # type: Callable[[], T]
):
# type: (...) -> T
"""Runs a callback in the context of the hub. Alternatively the
with statement can be used on the hub directly.
"""
with self:
return callback()
def get_integration(
self, name_or_class # type: Union[str, Type[Integration]]
):
# type: (...) -> Any
"""Returns the integration for this hub by name or class. If there
is no client bound or the client does not have that integration
then `None` is returned.
If the return value is not `None` the hub is guaranteed to have a
client attached.
"""
if isinstance(name_or_class, str):
integration_name = name_or_class
elif name_or_class.identifier is not None:
integration_name = name_or_class.identifier
else:
raise ValueError("Integration has no name")
client = self._stack[-1][0]
if client is not None:
rv = client.integrations.get(integration_name)
if rv is not None:
return rv
if _initial_client is not None:
initial_client = _initial_client()
else:
initial_client = None
if (
initial_client is not None
and initial_client is not client
and initial_client.integrations.get(integration_name) is not None
):
warning = (
"Integration %r attempted to run but it was only "
"enabled on init() but not the client that "
"was bound to the current flow. Earlier versions of "
"the SDK would consider these integrations enabled but "
"this is no longer the case." % (name_or_class,)
)
warn(Warning(warning), stacklevel=3)
logger.warning(warning)
@property
def client(self):
# type: () -> Optional[Client]
"""Returns the current client on the hub."""
return self._stack[-1][0]
def last_event_id(self):
# type: () -> Optional[str]
"""Returns the last event ID."""
return self._last_event_id
def bind_client(
self, new # type: Optional[Client]
):
# type: (...) -> None
"""Binds a new client to the hub."""
top = self._stack[-1]
self._stack[-1] = (new, top[1])
def capture_event(
self,
event, # type: Event
hint=None, # type: Optional[Hint]
):
# type: (...) -> Optional[str]
"""Captures an event. Alias of :py:meth:`sentry_sdk.Client.capture_event`.
"""
client, scope = self._stack[-1]
if client is not None:
rv = client.capture_event(event, hint, scope)
if rv is not None:
self._last_event_id = rv
return rv
return None
def capture_message(
self,
message, # type: str
level=None, # type: Optional[str]
):
# type: (...) -> Optional[str]
"""Captures a message. The message is just a string. If no level
is provided the default level is `info`.
:returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`).
"""
if self.client is None:
return None
if level is None:
level = "info"
return self.capture_event({"message": message, "level": level})
def capture_exception(
self, error=None # type: Optional[Union[BaseException, ExcInfo]]
):
# type: (...) -> Optional[str]
"""Captures an exception.
:param error: An exception to catch. If `None`, `sys.exc_info()` will be used.
:returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`).
"""
client = self.client
if client is None:
return None
if error is None:
exc_info = sys.exc_info()
else:
exc_info = exc_info_from_error(error)
event, hint = event_from_exception(exc_info, client_options=client.options)
try:
return self.capture_event(event, hint=hint)
except Exception:
self._capture_internal_exception(sys.exc_info())
return None
def _capture_internal_exception(
self, exc_info # type: Any
):
# type: (...) -> Any
"""
Capture an exception that is likely caused by a bug in the SDK
itself.
These exceptions do not end up in Sentry and are just logged instead.
"""
logger.error("Internal error in sentry_sdk", exc_info=exc_info) # type: ignore
def add_breadcrumb(
self,
crumb=None, # type: Optional[Breadcrumb]
hint=None, # type: Optional[BreadcrumbHint]
**kwargs # type: Any
):
# type: (...) -> None
"""
Adds a breadcrumb.
:param crumb: Dictionary with the data as the sentry v7/v8 protocol expects.
:param hint: An optional value that can be used by `before_breadcrumb`
to customize the breadcrumbs that are emitted.
"""
client, scope = self._stack[-1]
if client is None:
logger.info("Dropped breadcrumb because no client bound")
return
crumb = dict(crumb or ()) # type: Breadcrumb
crumb.update(kwargs)
if not crumb:
return
hint = dict(hint or ()) # type: Hint
if crumb.get("timestamp") is None:
crumb["timestamp"] = datetime.utcnow()
if crumb.get("type") is None:
crumb["type"] = "default"
if client.options["before_breadcrumb"] is not None:
new_crumb = client.options["before_breadcrumb"](crumb, hint)
else:
new_crumb = crumb
if new_crumb is not None:
scope._breadcrumbs.append(new_crumb)
else:
logger.info("before breadcrumb dropped breadcrumb (%s)", crumb)
max_breadcrumbs = client.options["max_breadcrumbs"] # type: int
while len(scope._breadcrumbs) > max_breadcrumbs:
scope._breadcrumbs.popleft()
def start_span(
self,
span=None, # type: Optional[Span]
**kwargs # type: Any
):
# type: (...) -> Span
"""
Create a new span whose parent span is the currently active
span, if any. The return value is the span object that can
be used as a context manager to start and stop timing.
Note that you will not see any span that is not contained
within a transaction. Create a transaction with
``start_span(transaction="my transaction")`` if an
integration doesn't already do this for you.
"""
client, scope = self._stack[-1]
kwargs.setdefault("hub", self)
if span is None:
if scope.span is not None:
span = scope.span.new_span(**kwargs)
else:
span = Span(**kwargs)
if span.sampled is None and span.transaction is not None:
sample_rate = client and client.options["traces_sample_rate"] or 0
span.sampled = random.random() < sample_rate
if span.sampled:
max_spans = (
client and client.options["_experiments"].get("max_spans") or 1000
)
span.init_finished_spans(maxlen=max_spans)
return span
@overload # noqa
def push_scope(
self, callback=None # type: Optional[None]
):
# type: (...) -> ContextManager[Scope]
pass
@overload # noqa
def push_scope(
self, callback # type: Callable[[Scope], None]
):
# type: (...) -> None
pass
def push_scope( # noqa
self, callback=None # type: Optional[Callable[[Scope], None]]
):
# type: (...) -> Optional[ContextManager[Scope]]
"""
Pushes a new layer on the scope stack.
:param callback: If provided, this method pushes a scope, calls
`callback`, and pops the scope again.
:returns: If no `callback` is provided, a context manager that should
be used to pop the scope again.
"""
if callback is not None:
with self.push_scope() as scope:
callback(scope)
return None
client, scope = self._stack[-1]
new_layer = (client, copy.copy(scope))
self._stack.append(new_layer)
return _ScopeManager(self)
scope = push_scope
def pop_scope_unsafe(self):
# type: () -> Tuple[Optional[Client], Scope]
"""
Pops a scope layer from the stack.
Try to use the context manager :py:meth:`push_scope` instead.
"""
rv = self._stack.pop()
assert self._stack, "stack must have at least one layer"
return rv
@overload # noqa
def configure_scope(
self, callback=None # type: Optional[None]
):
# type: (...) -> ContextManager[Scope]
pass
@overload # noqa
def configure_scope(
self, callback # type: Callable[[Scope], None]
):
# type: (...) -> None
pass
def configure_scope( # noqa
self, callback=None # type: Optional[Callable[[Scope], None]]
): # noqa
# type: (...) -> Optional[ContextManager[Scope]]
"""
Reconfigures the scope.
:param callback: If provided, call the callback with the current scope.
:returns: If no callback is provided, returns a context manager that returns the scope.
"""
client, scope = self._stack[-1]
if callback is not None:
if client is not None:
callback(scope)
return None
@contextmanager
def inner():
# type: () -> Generator[Scope, None, None]
if client is not None:
yield scope
else:
yield Scope()
return inner()
def flush(
self,
timeout=None, # type: Optional[float]
callback=None, # type: Optional[Callable[[int, float], None]]
):
# type: (...) -> None
"""
Alias for :py:meth:`sentry_sdk.Client.flush`
"""
client, scope = self._stack[-1]
if client is not None:
return client.flush(timeout=timeout, callback=callback)
def iter_trace_propagation_headers(self):
# type: () -> Generator[Tuple[str, str], None, None]
# TODO: Document
client, scope = self._stack[-1]
if scope._span is None:
return
propagate_traces = client and client.options["propagate_traces"]
if not propagate_traces:
return
if client and client.options["traceparent_v2"]:
traceparent = scope._span.to_traceparent()
else:
traceparent = scope._span.to_legacy_traceparent()
yield "sentry-trace", traceparent
GLOBAL_HUB = Hub()
_local.set(GLOBAL_HUB)

View File

@ -0,0 +1,127 @@
"""This package"""
from __future__ import absolute_import
from threading import Lock
from sentry_sdk._compat import iteritems
from sentry_sdk.utils import logger
from sentry_sdk._types import MYPY
if MYPY:
from typing import Iterator
from typing import Dict
from typing import List
from typing import Set
from typing import Type
from typing import Callable
_installer_lock = Lock()
_installed_integrations = set() # type: Set[str]
def _generate_default_integrations_iterator(*import_strings):
# type: (*str) -> Callable[[], Iterator[Type[Integration]]]
def iter_default_integrations():
# type: () -> Iterator[Type[Integration]]
"""Returns an iterator of the default integration classes:
"""
from importlib import import_module
for import_string in import_strings:
module, cls = import_string.rsplit(".", 1)
yield getattr(import_module(module), cls)
if isinstance(iter_default_integrations.__doc__, str):
for import_string in import_strings:
iter_default_integrations.__doc__ += "\n- `{}`".format(import_string)
return iter_default_integrations
iter_default_integrations = _generate_default_integrations_iterator(
"sentry_sdk.integrations.logging.LoggingIntegration",
"sentry_sdk.integrations.stdlib.StdlibIntegration",
"sentry_sdk.integrations.excepthook.ExcepthookIntegration",
"sentry_sdk.integrations.dedupe.DedupeIntegration",
"sentry_sdk.integrations.atexit.AtexitIntegration",
"sentry_sdk.integrations.modules.ModulesIntegration",
"sentry_sdk.integrations.argv.ArgvIntegration",
"sentry_sdk.integrations.threading.ThreadingIntegration",
)
del _generate_default_integrations_iterator
def setup_integrations(integrations, with_defaults=True):
# type: (List[Integration], bool) -> Dict[str, Integration]
"""Given a list of integration instances this installs them all. When
`with_defaults` is set to `True` then all default integrations are added
unless they were already provided before.
"""
integrations = dict(
(integration.identifier, integration) for integration in integrations or ()
)
logger.debug("Setting up integrations (with default = %s)", with_defaults)
if with_defaults:
for integration_cls in iter_default_integrations():
if integration_cls.identifier not in integrations:
instance = integration_cls()
integrations[instance.identifier] = instance
for identifier, integration in iteritems(integrations): # type: ignore
with _installer_lock:
if identifier not in _installed_integrations:
logger.debug(
"Setting up previously not enabled integration %s", identifier
)
try:
type(integration).setup_once()
except NotImplementedError:
if getattr(integration, "install", None) is not None:
logger.warning(
"Integration %s: The install method is "
"deprecated. Use `setup_once`.",
identifier,
)
integration.install()
else:
raise
_installed_integrations.add(identifier)
for identifier in integrations:
logger.debug("Enabling integration %s", identifier)
return integrations
class Integration(object):
"""Baseclass for all integrations.
To accept options for an integration, implement your own constructor that
saves those options on `self`.
"""
install = None
"""Legacy method, do not implement."""
identifier = None # type: str
"""String unique ID of integration type"""
@staticmethod
def setup_once():
# type: () -> None
"""
Initialize the integration.
This function is only called once, ever. Configuration is not available
at this point, so the only thing to do here is to hook into exception
handlers, and perhaps do monkeypatches.
Inside those hooks `Integration.current` can be used to access the
instance again.
"""
raise NotImplementedError()

View File

@ -0,0 +1,155 @@
import json
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.utils import AnnotatedValue
from sentry_sdk._compat import text_type, iteritems
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
from typing import Dict
from typing import Optional
from typing import Union
SENSITIVE_ENV_KEYS = (
"REMOTE_ADDR",
"HTTP_X_FORWARDED_FOR",
"HTTP_SET_COOKIE",
"HTTP_COOKIE",
"HTTP_AUTHORIZATION",
"HTTP_X_FORWARDED_FOR",
"HTTP_X_REAL_IP",
)
SENSITIVE_HEADERS = tuple(
x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_")
)
class RequestExtractor(object):
def __init__(self, request):
# type: (Any) -> None
self.request = request
def extract_into_event(self, event):
# type: (Dict[str, Any]) -> None
client = Hub.current.client
if client is None:
return
data = None # type: Optional[Union[AnnotatedValue, Dict[str, Any]]]
content_length = self.content_length()
request_info = event.setdefault("request", {})
if _should_send_default_pii():
request_info["cookies"] = dict(self.cookies())
bodies = client.options["request_bodies"]
if (
bodies == "never"
or (bodies == "small" and content_length > 10 ** 3)
or (bodies == "medium" and content_length > 10 ** 4)
):
data = AnnotatedValue(
"",
{"rem": [["!config", "x", 0, content_length]], "len": content_length},
)
else:
parsed_body = self.parsed_body()
if parsed_body is not None:
data = parsed_body
elif self.raw_data():
data = AnnotatedValue(
"",
{"rem": [["!raw", "x", 0, content_length]], "len": content_length},
)
else:
return
request_info["data"] = data
def content_length(self):
# type: () -> int
try:
return int(self.env().get("CONTENT_LENGTH", 0))
except ValueError:
return 0
def cookies(self):
raise NotImplementedError()
def raw_data(self):
raise NotImplementedError()
def form(self):
raise NotImplementedError()
def parsed_body(self):
# type: () -> Optional[Dict[str, Any]]
form = self.form()
files = self.files()
if form or files:
data = dict(iteritems(form))
for k, v in iteritems(files):
size = self.size_of_file(v)
data[k] = AnnotatedValue(
"", {"len": size, "rem": [["!raw", "x", 0, size]]}
)
return data
return self.json()
def is_json(self):
# type: () -> bool
return _is_json_content_type(self.env().get("CONTENT_TYPE"))
def json(self):
# type: () -> Optional[Any]
try:
if self.is_json():
raw_data = self.raw_data()
if not isinstance(raw_data, text_type):
raw_data = raw_data.decode("utf-8")
return json.loads(raw_data)
except ValueError:
pass
return None
def files(self):
raise NotImplementedError()
def size_of_file(self, file):
raise NotImplementedError()
def env(self):
raise NotImplementedError()
def _is_json_content_type(ct):
# type: (str) -> bool
mt = (ct or "").split(";", 1)[0]
return (
mt == "application/json"
or (mt.startswith("application/"))
and mt.endswith("+json")
)
def _filter_headers(headers):
# type: (Dict[str, str]) -> Dict[str, str]
if _should_send_default_pii():
return headers
return {
k: (
v
if k.upper().replace("-", "_") not in SENSITIVE_HEADERS
else AnnotatedValue("", {"rem": [["!config", "x", 0, len(v)]]})
)
for k, v in iteritems(headers)
}

View File

@ -0,0 +1,149 @@
import sys
import weakref
from sentry_sdk._compat import reraise
from sentry_sdk.hub import Hub
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations.logging import ignore_logger
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
transaction_from_function,
HAS_REAL_CONTEXTVARS,
)
import asyncio
from aiohttp.web import Application, HTTPException, UrlDispatcher # type: ignore
from sentry_sdk._types import MYPY
if MYPY:
from aiohttp.web_request import Request # type: ignore
from aiohttp.abc import AbstractMatchInfo # type: ignore
from typing import Any
from typing import Dict
from typing import Tuple
from typing import Callable
from sentry_sdk.utils import ExcInfo
from sentry_sdk._types import EventProcessor
class AioHttpIntegration(Integration):
identifier = "aiohttp"
@staticmethod
def setup_once():
# type: () -> None
if not HAS_REAL_CONTEXTVARS:
# We better have contextvars or we're going to leak state between
# requests.
raise RuntimeError(
"The aiohttp integration for Sentry requires Python 3.7+ "
" or aiocontextvars package"
)
ignore_logger("aiohttp.server")
old_handle = Application._handle
async def sentry_app_handle(self, request, *args, **kwargs):
# type: (Any, Request, *Any, **Any) -> Any
async def inner():
# type: () -> Any
hub = Hub.current
if hub.get_integration(AioHttpIntegration) is None:
return await old_handle(self, request, *args, **kwargs)
weak_request = weakref.ref(request)
with Hub(Hub.current) as hub:
with hub.configure_scope() as scope:
scope.clear_breadcrumbs()
scope.add_event_processor(_make_request_processor(weak_request))
# If this transaction name makes it to the UI, AIOHTTP's
# URL resolver did not find a route or died trying.
with hub.start_span(transaction="generic AIOHTTP request"):
try:
response = await old_handle(self, request)
except HTTPException:
raise
except Exception:
reraise(*_capture_exception(hub))
return response
# Explicitly wrap in task such that current contextvar context is
# copied. Just doing `return await inner()` will leak scope data
# between requests.
return await asyncio.get_event_loop().create_task(inner())
Application._handle = sentry_app_handle
old_urldispatcher_resolve = UrlDispatcher.resolve
async def sentry_urldispatcher_resolve(self, request):
# type: (UrlDispatcher, Request) -> AbstractMatchInfo
rv = await old_urldispatcher_resolve(self, request)
name = None
try:
name = transaction_from_function(rv.handler)
except Exception:
pass
if name is not None:
with Hub.current.configure_scope() as scope:
scope.transaction = name
return rv
UrlDispatcher.resolve = sentry_urldispatcher_resolve
def _make_request_processor(weak_request):
# type: (Callable[[], Request]) -> EventProcessor
def aiohttp_processor(
event, # type: Dict[str, Any]
hint, # type: Dict[str, Tuple[type, BaseException, Any]]
):
# type: (...) -> Dict[str, Any]
request = weak_request()
if request is None:
return event
with capture_internal_exceptions():
# TODO: Figure out what to do with request body. Methods on request
# are async, but event processors are not.
request_info = event.setdefault("request", {})
request_info["url"] = "%s://%s%s" % (
request.scheme,
request.host,
request.path,
)
request_info["query_string"] = request.query_string
request_info["method"] = request.method
request_info["env"] = {"REMOTE_ADDR": request.remote}
request_info["headers"] = _filter_headers(dict(request.headers))
return event
return aiohttp_processor
def _capture_exception(hub):
# type: (Hub) -> ExcInfo
exc_info = sys.exc_info()
event, hint = event_from_exception(
exc_info,
client_options=hub.client.options, # type: ignore
mechanism={"type": "aiohttp", "handled": False},
)
hub.capture_event(event, hint=hint)
return exc_info

View File

@ -0,0 +1,33 @@
from __future__ import absolute_import
import sys
from sentry_sdk.hub import Hub
from sentry_sdk.integrations import Integration
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk._types import MYPY
if MYPY:
from typing import Optional
from sentry_sdk._types import Event, Hint
class ArgvIntegration(Integration):
identifier = "argv"
@staticmethod
def setup_once():
# type: () -> None
@add_global_event_processor
def processor(event, hint):
# type: (Event, Optional[Hint]) -> Optional[Event]
if Hub.current.get_integration(ArgvIntegration) is not None:
extra = event.setdefault("extra", {})
# If some event processor decided to set extra to e.g. an
# `int`, don't crash. Not here.
if isinstance(extra, dict):
extra["sys.argv"] = sys.argv
return event

View File

@ -0,0 +1,154 @@
"""
An ASGI middleware.
Based on Tom Christie's `sentry-asgi <https://github.com/encode/sentry-asgi>`_.
"""
import functools
import urllib
from sentry_sdk._types import MYPY
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk.utils import ContextVar, event_from_exception, transaction_from_function
from sentry_sdk.tracing import Span
if MYPY:
from typing import Dict
from typing import Any
_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied")
def _capture_exception(hub, exc):
# type: (Hub, Any) -> None
# Check client here as it might have been unset while streaming response
if hub.client is not None:
event, hint = event_from_exception(
exc,
client_options=hub.client.options,
mechanism={"type": "asgi", "handled": False},
)
hub.capture_event(event, hint=hint)
class SentryAsgiMiddleware:
__slots__ = ("app",)
def __init__(self, app):
self.app = app
def __call__(self, scope, receive=None, send=None):
if receive is None or send is None:
async def run_asgi2(receive, send):
return await self._run_app(
scope, lambda: self.app(scope)(receive, send)
)
return run_asgi2
else:
return self._run_app(scope, lambda: self.app(scope, receive, send))
async def _run_app(self, scope, callback):
if _asgi_middleware_applied.get(False):
return await callback()
_asgi_middleware_applied.set(True)
try:
hub = Hub(Hub.current)
with hub:
with hub.configure_scope() as sentry_scope:
sentry_scope.clear_breadcrumbs()
sentry_scope._name = "asgi"
processor = functools.partial(
self.event_processor, asgi_scope=scope
)
sentry_scope.add_event_processor(processor)
if scope["type"] in ("http", "websocket"):
span = Span.continue_from_headers(dict(scope["headers"]))
span.op = "{}.server".format(scope["type"])
else:
span = Span()
span.op = "asgi.server"
span.set_tag("asgi.type", scope["type"])
span.transaction = "generic ASGI request"
with hub.start_span(span) as span:
try:
return await callback()
except Exception as exc:
_capture_exception(hub, exc)
raise exc from None
finally:
_asgi_middleware_applied.set(False)
def event_processor(self, event, hint, asgi_scope):
request_info = event.setdefault("request", {})
if asgi_scope["type"] in ("http", "websocket"):
request_info["url"] = self.get_url(asgi_scope)
request_info["method"] = asgi_scope["method"]
request_info["headers"] = _filter_headers(self.get_headers(asgi_scope))
request_info["query_string"] = self.get_query(asgi_scope)
if asgi_scope.get("client") and _should_send_default_pii():
request_info["env"] = {"REMOTE_ADDR": asgi_scope["client"][0]}
if asgi_scope.get("endpoint"):
# Webframeworks like Starlette mutate the ASGI env once routing is
# done, which is sometime after the request has started. If we have
# an endpoint, overwrite our path-based transaction name.
event["transaction"] = self.get_transaction(asgi_scope)
return event
def get_url(self, scope):
"""
Extract URL from the ASGI scope, without also including the querystring.
"""
scheme = scope.get("scheme", "http")
server = scope.get("server", None)
path = scope.get("root_path", "") + scope["path"]
for key, value in scope["headers"]:
if key == b"host":
host_header = value.decode("latin-1")
return "%s://%s%s" % (scheme, host_header, path)
if server is not None:
host, port = server
default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme]
if port != default_port:
return "%s://%s:%s%s" % (scheme, host, port, path)
return "%s://%s%s" % (scheme, host, path)
return path
def get_query(self, scope):
"""
Extract querystring from the ASGI scope, in the format that the Sentry protocol expects.
"""
return urllib.parse.unquote(scope["query_string"].decode("latin-1"))
def get_headers(self, scope):
"""
Extract headers from the ASGI scope, in the format that the Sentry protocol expects.
"""
headers = {} # type: Dict[str, str]
for raw_key, raw_value in scope["headers"]:
key = raw_key.decode("latin-1")
value = raw_value.decode("latin-1")
if key in headers:
headers[key] = headers[key] + ", " + value
else:
headers[key] = value
return headers
def get_transaction(self, scope):
"""
Return a transaction string to identify the routed endpoint.
"""
return transaction_from_function(scope["endpoint"])

View File

@ -0,0 +1,57 @@
from __future__ import absolute_import
import os
import sys
import atexit
from sentry_sdk.hub import Hub
from sentry_sdk.utils import logger
from sentry_sdk.integrations import Integration
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
from typing import Optional
def default_callback(pending, timeout):
"""This is the default shutdown callback that is set on the options.
It prints out a message to stderr that informs the user that some events
are still pending and the process is waiting for them to flush out.
"""
def echo(msg):
# type: (str) -> None
sys.stderr.write(msg + "\n")
echo("Sentry is attempting to send %i pending error messages" % pending)
echo("Waiting up to %s seconds" % timeout)
echo("Press Ctrl-%s to quit" % (os.name == "nt" and "Break" or "C"))
sys.stderr.flush()
class AtexitIntegration(Integration):
identifier = "atexit"
def __init__(self, callback=None):
# type: (Optional[Any]) -> None
if callback is None:
callback = default_callback
self.callback = callback
@staticmethod
def setup_once():
# type: () -> None
@atexit.register
def _shutdown():
logger.debug("atexit: got shutdown signal")
hub = Hub.main
integration = hub.get_integration(AtexitIntegration)
if integration is not None:
logger.debug("atexit: shutting down client")
# If an integration is there, a client has to be there.
client = hub.client # type: Any
client.close(callback=integration.callback)

View File

@ -0,0 +1,196 @@
import sys
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk._compat import reraise
from sentry_sdk.utils import (
AnnotatedValue,
capture_internal_exceptions,
event_from_exception,
logger,
)
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
def _wrap_handler(handler):
def sentry_handler(event, context, *args, **kwargs):
hub = Hub.current
integration = hub.get_integration(AwsLambdaIntegration)
if integration is None:
return handler(event, context, *args, **kwargs)
# If an integration is there, a client has to be there.
client = hub.client # type: Any
with hub.push_scope() as scope:
with capture_internal_exceptions():
scope.clear_breadcrumbs()
scope.transaction = context.function_name
scope.add_event_processor(_make_request_event_processor(event, context))
try:
return handler(event, context, *args, **kwargs)
except Exception:
exc_info = sys.exc_info()
event, hint = event_from_exception(
exc_info,
client_options=client.options,
mechanism={"type": "aws_lambda", "handled": False},
)
hub.capture_event(event, hint=hint)
reraise(*exc_info)
return sentry_handler
def _drain_queue():
with capture_internal_exceptions():
hub = Hub.current
integration = hub.get_integration(AwsLambdaIntegration)
if integration is not None:
# Flush out the event queue before AWS kills the
# process.
hub.flush()
class AwsLambdaIntegration(Integration):
identifier = "aws_lambda"
@staticmethod
def setup_once():
# type: () -> None
import __main__ as lambda_bootstrap # type: ignore
pre_37 = True # Python 3.6 or 2.7
if not hasattr(lambda_bootstrap, "handle_http_request"):
try:
import bootstrap as lambda_bootstrap # type: ignore
pre_37 = False # Python 3.7
except ImportError:
pass
if not hasattr(lambda_bootstrap, "handle_event_request"):
logger.warning(
"Not running in AWS Lambda environment, "
"AwsLambdaIntegration disabled"
)
return
if pre_37:
old_handle_event_request = lambda_bootstrap.handle_event_request
def sentry_handle_event_request(request_handler, *args, **kwargs):
request_handler = _wrap_handler(request_handler)
return old_handle_event_request(request_handler, *args, **kwargs)
lambda_bootstrap.handle_event_request = sentry_handle_event_request
old_handle_http_request = lambda_bootstrap.handle_http_request
def sentry_handle_http_request(request_handler, *args, **kwargs):
request_handler = _wrap_handler(request_handler)
return old_handle_http_request(request_handler, *args, **kwargs)
lambda_bootstrap.handle_http_request = sentry_handle_http_request
# Patch to_json to drain the queue. This should work even when the
# SDK is initialized inside of the handler
old_to_json = lambda_bootstrap.to_json
def sentry_to_json(*args, **kwargs):
_drain_queue()
return old_to_json(*args, **kwargs)
lambda_bootstrap.to_json = sentry_to_json
else:
old_handle_event_request = lambda_bootstrap.handle_event_request
def sentry_handle_event_request( # type: ignore
lambda_runtime_client, request_handler, *args, **kwargs
):
request_handler = _wrap_handler(request_handler)
return old_handle_event_request(
lambda_runtime_client, request_handler, *args, **kwargs
)
lambda_bootstrap.handle_event_request = sentry_handle_event_request
# Patch the runtime client to drain the queue. This should work
# even when the SDK is initialized inside of the handler
def _wrap_post_function(f):
def inner(*args, **kwargs):
_drain_queue()
return f(*args, **kwargs)
return inner
lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = _wrap_post_function(
lambda_bootstrap.LambdaRuntimeClient.post_invocation_result
)
lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = _wrap_post_function(
lambda_bootstrap.LambdaRuntimeClient.post_invocation_error
)
def _make_request_event_processor(aws_event, aws_context):
def event_processor(event, hint):
extra = event.setdefault("extra", {})
extra["lambda"] = {
"remaining_time_in_millis": aws_context.get_remaining_time_in_millis(),
"function_name": aws_context.function_name,
"function_version": aws_context.function_version,
"invoked_function_arn": aws_context.invoked_function_arn,
"aws_request_id": aws_context.aws_request_id,
}
request = event.setdefault("request", {})
if "httpMethod" in aws_event:
request["method"] = aws_event["httpMethod"]
request["url"] = _get_url(aws_event, aws_context)
if "queryStringParameters" in aws_event:
request["query_string"] = aws_event["queryStringParameters"]
if "headers" in aws_event:
request["headers"] = _filter_headers(aws_event["headers"])
if aws_event.get("body", None):
# Unfortunately couldn't find a way to get structured body from AWS
# event. Meaning every body is unstructured to us.
request["data"] = AnnotatedValue("", {"rem": [["!raw", "x", 0, 0]]})
if _should_send_default_pii():
user_info = event.setdefault("user", {})
id = aws_event.get("identity", {}).get("userArn")
if id is not None:
user_info["id"] = id
ip = aws_event.get("identity", {}).get("sourceIp")
if ip is not None:
user_info["ip_address"] = ip
return event
return event_processor
def _get_url(event, context):
path = event.get("path", None)
headers = event.get("headers", {})
host = headers.get("Host", None)
proto = headers.get("X-Forwarded-Proto", None)
if proto and host and path:
return "{}://{}{}".format(proto, host, path)
return "awslambda:///{}".format(context.function_name)

View File

@ -0,0 +1,156 @@
from __future__ import absolute_import
import sys
import types
from functools import wraps
from sentry_sdk.hub import Hub
from sentry_sdk._compat import reraise
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations.logging import ignore_logger
WRAPPED_FUNC = "_wrapped_{}_"
INSPECT_FUNC = "_inspect_{}" # Required format per apache_beam/transforms/core.py
USED_FUNC = "_sentry_used_"
class BeamIntegration(Integration):
identifier = "beam"
@staticmethod
def setup_once():
# type: () -> None
from apache_beam.transforms.core import DoFn, ParDo # type: ignore
ignore_logger("root")
ignore_logger("bundle_processor.create")
function_patches = ["process", "start_bundle", "finish_bundle", "setup"]
for func_name in function_patches:
setattr(
DoFn,
INSPECT_FUNC.format(func_name),
_wrap_inspect_call(DoFn, func_name),
)
old_init = ParDo.__init__
def sentry_init_pardo(self, fn, *args, **kwargs):
# Do not monkey patch init twice
if not getattr(self, "_sentry_is_patched", False):
for func_name in function_patches:
if not hasattr(fn, func_name):
continue
wrapped_func = WRAPPED_FUNC.format(func_name)
# Check to see if inspect is set and process is not
# to avoid monkey patching process twice.
# Check to see if function is part of object for
# backwards compatibility.
process_func = getattr(fn, func_name)
inspect_func = getattr(fn, INSPECT_FUNC.format(func_name))
if not getattr(inspect_func, USED_FUNC, False) and not getattr(
process_func, USED_FUNC, False
):
setattr(fn, wrapped_func, process_func)
setattr(fn, func_name, _wrap_task_call(process_func))
self._sentry_is_patched = True
old_init(self, fn, *args, **kwargs)
ParDo.__init__ = sentry_init_pardo
def _wrap_inspect_call(cls, func_name):
from apache_beam.typehints.decorators import getfullargspec # type: ignore
if not hasattr(cls, func_name):
return None
def _inspect(self):
"""
Inspect function overrides the way Beam gets argspec.
"""
wrapped_func = WRAPPED_FUNC.format(func_name)
if hasattr(self, wrapped_func):
process_func = getattr(self, wrapped_func)
else:
process_func = getattr(self, func_name)
setattr(self, func_name, _wrap_task_call(process_func))
setattr(self, wrapped_func, process_func)
# getfullargspec is deprecated in more recent beam versions and get_function_args_defaults
# (which uses Signatures internally) should be used instead.
try:
from apache_beam.transforms.core import get_function_args_defaults
return get_function_args_defaults(process_func)
except ImportError:
return getfullargspec(process_func)
setattr(_inspect, USED_FUNC, True)
return _inspect
def _wrap_task_call(func):
"""
Wrap task call with a try catch to get exceptions.
Pass the client on to raise_exception so it can get rebinded.
"""
client = Hub.current.client
@wraps(func)
def _inner(*args, **kwargs):
try:
gen = func(*args, **kwargs)
except Exception:
raise_exception(client)
if not isinstance(gen, types.GeneratorType):
return gen
return _wrap_generator_call(gen, client)
setattr(_inner, USED_FUNC, True)
return _inner
def _capture_exception(exc_info, hub):
"""
Send Beam exception to Sentry.
"""
integration = hub.get_integration(BeamIntegration)
if integration:
client = hub.client
event, hint = event_from_exception(
exc_info,
client_options=client.options,
mechanism={"type": "beam", "handled": False},
)
hub.capture_event(event, hint=hint)
def raise_exception(client):
"""
Raise an exception. If the client is not in the hub, rebind it.
"""
hub = Hub.current
if hub.client is None:
hub.bind_client(client)
exc_info = sys.exc_info()
with capture_internal_exceptions():
_capture_exception(exc_info, hub)
reraise(*exc_info)
def _wrap_generator_call(gen, client):
"""
Wrap the generator to handle any failures.
"""
while True:
try:
yield next(gen)
except StopIteration:
break
except Exception:
raise_exception(client)

View File

@ -0,0 +1,182 @@
from __future__ import absolute_import
from sentry_sdk.hub import Hub
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
transaction_from_function,
)
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk._types import MYPY
if MYPY:
from sentry_sdk.integrations.wsgi import _ScopedResponse
from typing import Any
from typing import Dict
from typing import Callable
from typing import Optional
from bottle import FileUpload, FormsDict, LocalRequest # type: ignore
from bottle import (
Bottle,
Route,
request as bottle_request,
HTTPResponse,
) # type: ignore
class BottleIntegration(Integration):
identifier = "bottle"
transaction_style = None
def __init__(self, transaction_style="endpoint"):
# type: (str) -> None
TRANSACTION_STYLE_VALUES = ("endpoint", "url")
if transaction_style not in TRANSACTION_STYLE_VALUES:
raise ValueError(
"Invalid value for transaction_style: %s (must be in %s)"
% (transaction_style, TRANSACTION_STYLE_VALUES)
)
self.transaction_style = transaction_style
@staticmethod
def setup_once():
# type: () -> None
# monkey patch method Bottle.__call__
old_app = Bottle.__call__
def sentry_patched_wsgi_app(self, environ, start_response):
# type: (Any, Dict[str, str], Callable) -> _ScopedResponse
hub = Hub.current
integration = hub.get_integration(BottleIntegration)
if integration is None:
return old_app(self, environ, start_response)
return SentryWsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))(
environ, start_response
)
Bottle.__call__ = sentry_patched_wsgi_app # type: ignore
# monkey patch method Bottle._handle
old_handle = Bottle._handle
def _patched_handle(self, environ):
hub = Hub.current
integration = hub.get_integration(BottleIntegration)
if integration is None:
return old_handle(self, environ)
# create new scope
scope_manager = hub.push_scope()
with scope_manager:
app = self
with hub.configure_scope() as scope:
scope._name = "bottle"
scope.add_event_processor(
_make_request_event_processor(app, bottle_request, integration)
)
res = old_handle(self, environ)
# scope cleanup
return res
Bottle._handle = _patched_handle
# monkey patch method Route._make_callback
old_make_callback = Route._make_callback
def patched_make_callback(self, *args, **kwargs):
hub = Hub.current
integration = hub.get_integration(BottleIntegration)
prepared_callback = old_make_callback(self, *args, **kwargs)
if integration is None:
return prepared_callback
# If an integration is there, a client has to be there.
client = hub.client # type: Any
def wrapped_callback(*args, **kwargs):
def capture_exception(exception):
event, hint = event_from_exception(
exception,
client_options=client.options,
mechanism={"type": "bottle", "handled": False},
)
hub.capture_event(event, hint=hint)
try:
res = prepared_callback(*args, **kwargs)
except HTTPResponse:
raise
except Exception as exception:
capture_exception(exception)
raise exception
return res
return wrapped_callback
Route._make_callback = patched_make_callback
class BottleRequestExtractor(RequestExtractor):
def env(self):
# type: () -> Dict[str, str]
return self.request.environ
def cookies(self):
# type: () -> Dict[str, str]
return self.request.cookies
def raw_data(self):
# type: () -> bytes
return self.request.body.read()
def form(self):
# type: () -> FormsDict
if self.is_json():
return None
return self.request.forms.decode()
def files(self):
# type: () -> Optional[Dict[str, str]]
if self.is_json():
return None
return self.request.files
def size_of_file(self, file):
# type: (FileUpload) -> int
return file.content_length
def _make_request_event_processor(app, request, integration):
# type: (Bottle, LocalRequest, BottleIntegration) -> Callable
def inner(event, hint):
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
try:
if integration.transaction_style == "endpoint":
event["transaction"] = request.route.name or transaction_from_function(
request.route.callback
)
elif integration.transaction_style == "url":
event["transaction"] = request.route.rule # type: ignore
except Exception:
pass
with capture_internal_exceptions():
BottleRequestExtractor(request).extract_into_event(event)
return event
return inner

View File

@ -0,0 +1,209 @@
from __future__ import absolute_import
import functools
import sys
from celery.exceptions import ( # type: ignore
SoftTimeLimitExceeded,
Retry,
Ignore,
Reject,
)
from sentry_sdk.hub import Hub
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
from sentry_sdk.tracing import Span
from sentry_sdk._compat import reraise
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations.logging import ignore_logger
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
CELERY_CONTROL_FLOW_EXCEPTIONS = (Retry, Ignore, Reject)
class CeleryIntegration(Integration):
identifier = "celery"
def __init__(self, propagate_traces=True):
# type: (bool) -> None
self.propagate_traces = propagate_traces
@staticmethod
def setup_once():
# type: () -> None
import celery.app.trace as trace # type: ignore
old_build_tracer = trace.build_tracer
def sentry_build_tracer(name, task, *args, **kwargs):
if not getattr(task, "_sentry_is_patched", False):
# Need to patch both methods because older celery sometimes
# short-circuits to task.run if it thinks it's safe.
task.__call__ = _wrap_task_call(task, task.__call__)
task.run = _wrap_task_call(task, task.run)
task.apply_async = _wrap_apply_async(task, task.apply_async)
# `build_tracer` is apparently called for every task
# invocation. Can't wrap every celery task for every invocation
# or we will get infinitely nested wrapper functions.
task._sentry_is_patched = True
return _wrap_tracer(task, old_build_tracer(name, task, *args, **kwargs))
trace.build_tracer = sentry_build_tracer
_patch_worker_exit()
# This logger logs every status of every task that ran on the worker.
# Meaning that every task's breadcrumbs are full of stuff like "Task
# <foo> raised unexpected <bar>".
ignore_logger("celery.worker.job")
ignore_logger("celery.app.trace")
def _wrap_apply_async(task, f):
@functools.wraps(f)
def apply_async(*args, **kwargs):
hub = Hub.current
integration = hub.get_integration(CeleryIntegration)
if integration is not None and integration.propagate_traces:
headers = None
for key, value in hub.iter_trace_propagation_headers():
if headers is None:
headers = dict(kwargs.get("headers") or {})
headers[key] = value
if headers is not None:
kwargs["headers"] = headers
with hub.start_span(op="celery.submit", description=task.name):
return f(*args, **kwargs)
else:
return f(*args, **kwargs)
return apply_async
def _wrap_tracer(task, f):
# Need to wrap tracer for pushing the scope before prerun is sent, and
# popping it after postrun is sent.
#
# This is the reason we don't use signals for hooking in the first place.
# Also because in Celery 3, signal dispatch returns early if one handler
# crashes.
@functools.wraps(f)
def _inner(*args, **kwargs):
hub = Hub.current
if hub.get_integration(CeleryIntegration) is None:
return f(*args, **kwargs)
with hub.push_scope() as scope:
scope._name = "celery"
scope.clear_breadcrumbs()
scope.add_event_processor(_make_event_processor(task, *args, **kwargs))
span = Span.continue_from_headers(args[3].get("headers") or {})
span.op = "celery.task"
span.transaction = "unknown celery task"
with capture_internal_exceptions():
# Celery task objects are not a thing to be trusted. Even
# something such as attribute access can fail.
span.transaction = task.name
with hub.start_span(span):
return f(*args, **kwargs)
return _inner
def _wrap_task_call(task, f):
# Need to wrap task call because the exception is caught before we get to
# see it. Also celery's reported stacktrace is untrustworthy.
# functools.wraps is important here because celery-once looks at this
# method's name.
# https://github.com/getsentry/sentry-python/issues/421
@functools.wraps(f)
def _inner(*args, **kwargs):
try:
return f(*args, **kwargs)
except Exception:
exc_info = sys.exc_info()
with capture_internal_exceptions():
_capture_exception(task, exc_info)
reraise(*exc_info)
return _inner
def _make_event_processor(task, uuid, args, kwargs, request=None):
def event_processor(event, hint):
with capture_internal_exceptions():
extra = event.setdefault("extra", {})
extra["celery-job"] = {
"task_name": task.name,
"args": args,
"kwargs": kwargs,
}
if "exc_info" in hint:
with capture_internal_exceptions():
if issubclass(hint["exc_info"][0], SoftTimeLimitExceeded):
event["fingerprint"] = [
"celery",
"SoftTimeLimitExceeded",
getattr(task, "name", task),
]
return event
return event_processor
def _capture_exception(task, exc_info):
hub = Hub.current
if hub.get_integration(CeleryIntegration) is None:
return
if isinstance(exc_info[1], CELERY_CONTROL_FLOW_EXCEPTIONS):
return
if hasattr(task, "throws") and isinstance(exc_info[1], task.throws):
return
# If an integration is there, a client has to be there.
client = hub.client # type: Any
event, hint = event_from_exception(
exc_info,
client_options=client.options,
mechanism={"type": "celery", "handled": False},
)
hub.capture_event(event, hint=hint)
with capture_internal_exceptions():
with hub.configure_scope() as scope:
scope.span.set_failure()
def _patch_worker_exit():
# Need to flush queue before worker shutdown because a crashing worker will
# call os._exit
from billiard.pool import Worker # type: ignore
old_workloop = Worker.workloop
def sentry_workloop(*args, **kwargs):
try:
return old_workloop(*args, **kwargs)
finally:
with capture_internal_exceptions():
hub = Hub.current
if hub.get_integration(CeleryIntegration) is not None:
hub.flush()
Worker.workloop = sentry_workloop

View File

@ -0,0 +1,43 @@
from sentry_sdk.hub import Hub
from sentry_sdk.utils import ContextVar
from sentry_sdk.integrations import Integration
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk._types import MYPY
if MYPY:
from typing import Optional
from sentry_sdk._types import Event, Hint
class DedupeIntegration(Integration):
identifier = "dedupe"
def __init__(self):
# type: () -> None
self._last_seen = ContextVar("last-seen")
@staticmethod
def setup_once():
# type: () -> None
@add_global_event_processor
def processor(event, hint):
# type: (Event, Optional[Hint]) -> Optional[Event]
if hint is None:
return event
integration = Hub.current.get_integration(DedupeIntegration)
if integration is None:
return event
exc_info = hint.get("exc_info", None)
if exc_info is None:
return event
exc = exc_info[1]
if integration._last_seen.get(None) is exc:
return None
integration._last_seen.set(exc)
return event

View File

@ -0,0 +1,448 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import sys
import threading
import weakref
from django import VERSION as DJANGO_VERSION # type: ignore
from django.core import signals # type: ignore
from sentry_sdk._types import MYPY
from sentry_sdk.utils import HAS_REAL_CONTEXTVARS
if MYPY:
from typing import Any
from typing import Callable
from typing import Dict
from typing import Optional
from typing import Union
from django.core.handlers.wsgi import WSGIRequest # type: ignore
from django.http.response import HttpResponse # type: ignore
from django.http.request import QueryDict # type: ignore
from django.utils.datastructures import MultiValueDict # type: ignore
from sentry_sdk.integrations.wsgi import _ScopedResponse
from sentry_sdk._types import Event, Hint
try:
from django.urls import resolve # type: ignore
except ImportError:
from django.core.urlresolvers import resolve # type: ignore
from sentry_sdk import Hub
from sentry_sdk.hub import _should_send_default_pii
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk.serializer import add_global_repr_processor
from sentry_sdk.tracing import record_sql_queries
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
transaction_from_function,
walk_exception_chain,
)
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations.logging import ignore_logger
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER
from sentry_sdk.integrations.django.templates import get_template_frame_from_exception
from sentry_sdk.integrations.django.middleware import patch_django_middlewares
if DJANGO_VERSION < (1, 10):
def is_authenticated(request_user):
# type: (Any) -> bool
return request_user.is_authenticated()
else:
def is_authenticated(request_user):
# type: (Any) -> bool
return request_user.is_authenticated
class DjangoIntegration(Integration):
identifier = "django"
transaction_style = None
middleware_spans = None
def __init__(self, transaction_style="url", middleware_spans=True):
# type: (str, bool) -> None
TRANSACTION_STYLE_VALUES = ("function_name", "url")
if transaction_style not in TRANSACTION_STYLE_VALUES:
raise ValueError(
"Invalid value for transaction_style: %s (must be in %s)"
% (transaction_style, TRANSACTION_STYLE_VALUES)
)
self.transaction_style = transaction_style
self.middleware_spans = middleware_spans
@staticmethod
def setup_once():
# type: () -> None
install_sql_hook()
# Patch in our custom middleware.
# logs an error for every 500
ignore_logger("django.server")
ignore_logger("django.request")
from django.core.handlers.wsgi import WSGIHandler
old_app = WSGIHandler.__call__
def sentry_patched_wsgi_handler(self, environ, start_response):
# type: (Any, Dict[str, str], Callable) -> _ScopedResponse
if Hub.current.get_integration(DjangoIntegration) is None:
return old_app(self, environ, start_response)
bound_old_app = old_app.__get__(self, WSGIHandler)
return SentryWsgiMiddleware(bound_old_app)(environ, start_response)
WSGIHandler.__call__ = sentry_patched_wsgi_handler
# patch get_response, because at that point we have the Django request
# object
from django.core.handlers.base import BaseHandler # type: ignore
old_get_response = BaseHandler.get_response
def sentry_patched_get_response(self, request):
# type: (Any, WSGIRequest) -> Union[HttpResponse, BaseException]
hub = Hub.current
integration = hub.get_integration(DjangoIntegration)
if integration is not None:
_patch_drf()
with hub.configure_scope() as scope:
# Rely on WSGI middleware to start a trace
try:
if integration.transaction_style == "function_name":
scope.transaction = transaction_from_function(
resolve(request.path).func
)
elif integration.transaction_style == "url":
scope.transaction = LEGACY_RESOLVER.resolve(request.path)
except Exception:
pass
scope.add_event_processor(
_make_event_processor(weakref.ref(request), integration)
)
return old_get_response(self, request)
BaseHandler.get_response = sentry_patched_get_response
signals.got_request_exception.connect(_got_request_exception)
@add_global_event_processor
def process_django_templates(event, hint):
# type: (Event, Optional[Hint]) -> Optional[Event]
if hint is None:
return event
exc_info = hint.get("exc_info", None)
if exc_info is None:
return event
exception = event.get("exception", None)
if exception is None:
return event
values = exception.get("values", None)
if values is None:
return event
for exception, (_, exc_value, _) in zip(
reversed(values), walk_exception_chain(exc_info)
):
frame = get_template_frame_from_exception(exc_value)
if frame is not None:
frames = exception.get("stacktrace", {}).get("frames", [])
for i in reversed(range(len(frames))):
f = frames[i]
if (
f.get("function") in ("parse", "render")
and f.get("module") == "django.template.base"
):
i += 1
break
else:
i = len(frames)
frames.insert(i, frame)
return event
@add_global_repr_processor
def _django_queryset_repr(value, hint):
try:
# Django 1.6 can fail to import `QuerySet` when Django settings
# have not yet been initialized.
#
# If we fail to import, return `NotImplemented`. It's at least
# unlikely that we have a query set in `value` when importing
# `QuerySet` fails.
from django.db.models.query import QuerySet # type: ignore
except Exception:
return NotImplemented
if not isinstance(value, QuerySet) or value._result_cache:
return NotImplemented
# Do not call Hub.get_integration here. It is intentional that
# running under a new hub does not suddenly start executing
# querysets. This might be surprising to the user but it's likely
# less annoying.
return u"<%s from %s at 0x%x>" % (
value.__class__.__name__,
value.__module__,
id(value),
)
_patch_channels()
patch_django_middlewares()
_DRF_PATCHED = False
_DRF_PATCH_LOCK = threading.Lock()
def _patch_drf():
"""
Patch Django Rest Framework for more/better request data. DRF's request
type is a wrapper around Django's request type. The attribute we're
interested in is `request.data`, which is a cached property containing a
parsed request body. Reading a request body from that property is more
reliable than reading from any of Django's own properties, as those don't
hold payloads in memory and therefore can only be accessed once.
We patch the Django request object to include a weak backreference to the
DRF request object, such that we can later use either in
`DjangoRequestExtractor`.
This function is not called directly on SDK setup, because importing almost
any part of Django Rest Framework will try to access Django settings (where
`sentry_sdk.init()` might be called from in the first place). Instead we
run this function on every request and do the patching on the first
request.
"""
global _DRF_PATCHED
if _DRF_PATCHED:
# Double-checked locking
return
with _DRF_PATCH_LOCK:
if _DRF_PATCHED:
return
# We set this regardless of whether the code below succeeds or fails.
# There is no point in trying to patch again on the next request.
_DRF_PATCHED = True
with capture_internal_exceptions():
try:
from rest_framework.views import APIView # type: ignore
except ImportError:
pass
else:
old_drf_initial = APIView.initial
def sentry_patched_drf_initial(self, request, *args, **kwargs):
with capture_internal_exceptions():
request._request._sentry_drf_request_backref = weakref.ref(
request
)
pass
return old_drf_initial(self, request, *args, **kwargs)
APIView.initial = sentry_patched_drf_initial
def _patch_channels():
try:
from channels.http import AsgiHandler # type: ignore
except ImportError:
return
if not HAS_REAL_CONTEXTVARS:
# We better have contextvars or we're going to leak state between
# requests.
raise RuntimeError(
"We detected that you are using Django channels 2.0. To get proper "
"instrumentation for ASGI requests, the Sentry SDK requires "
"Python 3.7+ or the aiocontextvars package from PyPI."
)
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
old_app = AsgiHandler.__call__
def sentry_patched_asgi_handler(self, receive, send):
if Hub.current.get_integration(DjangoIntegration) is None:
return old_app(receive, send)
middleware = SentryAsgiMiddleware(
lambda _scope: old_app.__get__(self, AsgiHandler)
)
return middleware(self.scope)(receive, send)
AsgiHandler.__call__ = sentry_patched_asgi_handler
def _make_event_processor(weak_request, integration):
# type: (Callable[[], WSGIRequest], DjangoIntegration) -> Callable
def event_processor(event, hint):
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
# if the request is gone we are fine not logging the data from
# it. This might happen if the processor is pushed away to
# another thread.
request = weak_request()
if request is None:
return event
try:
drf_request = request._sentry_drf_request_backref()
if drf_request is not None:
request = drf_request
except AttributeError:
pass
with capture_internal_exceptions():
DjangoRequestExtractor(request).extract_into_event(event)
if _should_send_default_pii():
with capture_internal_exceptions():
_set_user_info(request, event)
return event
return event_processor
def _got_request_exception(request=None, **kwargs):
# type: (WSGIRequest, **Any) -> None
hub = Hub.current
integration = hub.get_integration(DjangoIntegration)
if integration is not None:
# If an integration is there, a client has to be there.
client = hub.client # type: Any
event, hint = event_from_exception(
sys.exc_info(),
client_options=client.options,
mechanism={"type": "django", "handled": False},
)
hub.capture_event(event, hint=hint)
class DjangoRequestExtractor(RequestExtractor):
def env(self):
# type: () -> Dict[str, str]
return self.request.META
def cookies(self):
# type: () -> Dict[str, str]
return self.request.COOKIES
def raw_data(self):
# type: () -> bytes
return self.request.body
def form(self):
# type: () -> QueryDict
return self.request.POST
def files(self):
# type: () -> MultiValueDict
return self.request.FILES
def size_of_file(self, file):
return file.size
def parsed_body(self):
try:
return self.request.data
except AttributeError:
return RequestExtractor.parsed_body(self)
def _set_user_info(request, event):
# type: (WSGIRequest, Dict[str, Any]) -> None
user_info = event.setdefault("user", {})
user = getattr(request, "user", None)
if user is None or not is_authenticated(user):
return
try:
user_info["id"] = str(user.pk)
except Exception:
pass
try:
user_info["email"] = user.email
except Exception:
pass
try:
user_info["username"] = user.get_username()
except Exception:
pass
def install_sql_hook():
# type: () -> None
"""If installed this causes Django's queries to be captured."""
try:
from django.db.backends.utils import CursorWrapper # type: ignore
except ImportError:
from django.db.backends.util import CursorWrapper # type: ignore
try:
real_execute = CursorWrapper.execute
real_executemany = CursorWrapper.executemany
except AttributeError:
# This won't work on Django versions < 1.6
return
def execute(self, sql, params=None):
hub = Hub.current
if hub.get_integration(DjangoIntegration) is None:
return real_execute(self, sql, params)
with record_sql_queries(
hub, self.cursor, sql, params, paramstyle="format", executemany=False
):
return real_execute(self, sql, params)
def executemany(self, sql, param_list):
hub = Hub.current
if hub.get_integration(DjangoIntegration) is None:
return real_executemany(self, sql, param_list)
with record_sql_queries(
hub, self.cursor, sql, param_list, paramstyle="format", executemany=True
):
return real_executemany(self, sql, param_list)
CursorWrapper.execute = execute
CursorWrapper.executemany = executemany
ignore_logger("django.db.backends")

View File

@ -0,0 +1,106 @@
"""
Create spans from Django middleware invocations
"""
from functools import wraps
from django import VERSION as DJANGO_VERSION # type: ignore
from sentry_sdk import Hub
from sentry_sdk.utils import ContextVar, transaction_from_function
_import_string_should_wrap_middleware = ContextVar(
"import_string_should_wrap_middleware"
)
if DJANGO_VERSION < (1, 7):
import_string_name = "import_by_path"
else:
import_string_name = "import_string"
def patch_django_middlewares():
from django.core.handlers import base
old_import_string = getattr(base, import_string_name)
def sentry_patched_import_string(dotted_path):
rv = old_import_string(dotted_path)
if _import_string_should_wrap_middleware.get(None):
rv = _wrap_middleware(rv, dotted_path)
return rv
setattr(base, import_string_name, sentry_patched_import_string)
old_load_middleware = base.BaseHandler.load_middleware
def sentry_patched_load_middleware(self):
_import_string_should_wrap_middleware.set(True)
try:
return old_load_middleware(self)
finally:
_import_string_should_wrap_middleware.set(False)
base.BaseHandler.load_middleware = sentry_patched_load_middleware
def _wrap_middleware(middleware, middleware_name):
from sentry_sdk.integrations.django import DjangoIntegration
def _get_wrapped_method(old_method):
@wraps(old_method)
def sentry_wrapped_method(*args, **kwargs):
hub = Hub.current
integration = hub.get_integration(DjangoIntegration)
if integration is None or not integration.middleware_spans:
return old_method(*args, **kwargs)
function_name = transaction_from_function(old_method)
description = middleware_name
function_basename = getattr(old_method, "__name__", None)
if function_basename:
description = "{}.{}".format(description, function_basename)
with hub.start_span(
op="django.middleware", description=description
) as span:
span.set_tag("django.function_name", function_name)
span.set_tag("django.middleware_name", middleware_name)
return old_method(*args, **kwargs)
return sentry_wrapped_method
class SentryWrappingMiddleware(object):
def __init__(self, *args, **kwargs):
self._inner = middleware(*args, **kwargs)
self._call_method = None
# We need correct behavior for `hasattr()`, which we can only determine
# when we have an instance of the middleware we're wrapping.
def __getattr__(self, method_name):
if method_name not in (
"process_request",
"process_view",
"process_template_response",
"process_response",
"process_exception",
):
raise AttributeError()
old_method = getattr(self._inner, method_name)
rv = _get_wrapped_method(old_method)
self.__dict__[method_name] = rv
return rv
def __call__(self, *args, **kwargs):
if self._call_method is None:
self._call_method = _get_wrapped_method(self._inner.__call__)
return self._call_method(*args, **kwargs)
if hasattr(middleware, "__name__"):
SentryWrappingMiddleware.__name__ = middleware.__name__
return SentryWrappingMiddleware

View File

@ -0,0 +1,117 @@
from django.template import TemplateSyntaxError # type: ignore
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
from typing import Dict
from typing import Optional
try:
# support Django 1.9
from django.template.base import Origin # type: ignore
except ImportError:
# backward compatibility
from django.template.loader import LoaderOrigin as Origin # type: ignore
def get_template_frame_from_exception(exc_value):
# type: (Optional[BaseException]) -> Optional[Dict[str, Any]]
# As of Django 1.9 or so the new template debug thing showed up.
if hasattr(exc_value, "template_debug"):
return _get_template_frame_from_debug(exc_value.template_debug) # type: ignore
# As of r16833 (Django) all exceptions may contain a
# ``django_template_source`` attribute (rather than the legacy
# ``TemplateSyntaxError.source`` check)
if hasattr(exc_value, "django_template_source"):
return _get_template_frame_from_source(
exc_value.django_template_source # type: ignore
)
if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"):
source = exc_value.source
if isinstance(source, (tuple, list)) and isinstance(source[0], Origin):
return _get_template_frame_from_source(source)
return None
def _get_template_frame_from_debug(debug):
# type: (Dict[str, Any]) -> Dict[str, Any]
if debug is None:
return None
lineno = debug["line"]
filename = debug["name"]
if filename is None:
filename = "<django template>"
pre_context = []
post_context = []
context_line = None
for i, line in debug["source_lines"]:
if i < lineno:
pre_context.append(line)
elif i > lineno:
post_context.append(line)
else:
context_line = line
return {
"filename": filename,
"lineno": lineno,
"pre_context": pre_context[-5:],
"post_context": post_context[:5],
"context_line": context_line,
"in_app": True,
}
def _linebreak_iter(template_source):
yield 0
p = template_source.find("\n")
while p >= 0:
yield p + 1
p = template_source.find("\n", p + 1)
def _get_template_frame_from_source(source):
if not source:
return None
origin, (start, end) = source
filename = getattr(origin, "loadname", None)
if filename is None:
filename = "<django template>"
template_source = origin.reload()
lineno = None
upto = 0
pre_context = []
post_context = []
context_line = None
for num, next in enumerate(_linebreak_iter(template_source)):
line = template_source[upto:next]
if start >= upto and end <= next:
lineno = num
context_line = line
elif lineno is None:
pre_context.append(line)
else:
post_context.append(line)
upto = next
if context_line is None or lineno is None:
return None
return {
"filename": filename,
"lineno": lineno,
"pre_context": pre_context[-5:],
"post_context": post_context[:5],
"context_line": context_line,
}

View File

@ -0,0 +1,134 @@
"""
Copied from raven-python. Used for
`DjangoIntegration(transaction_fron="raven_legacy")`.
"""
from __future__ import absolute_import
import re
from sentry_sdk._types import MYPY
if MYPY:
from django.urls.resolvers import URLResolver # type: ignore
from typing import Dict
from typing import List
from typing import Optional
from django.urls.resolvers import URLPattern # type: ignore
from typing import Tuple
from typing import Union
from re import Pattern # type: ignore
try:
from django.urls import get_resolver # type: ignore
except ImportError:
from django.core.urlresolvers import get_resolver # type: ignore
def get_regex(resolver_or_pattern):
# type: (Union[URLPattern, URLResolver]) -> Pattern
"""Utility method for django's deprecated resolver.regex"""
try:
regex = resolver_or_pattern.regex
except AttributeError:
regex = resolver_or_pattern.pattern.regex
return regex
class RavenResolver(object):
_optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)")
_named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)")
_non_named_group_matcher = re.compile(r"\([^\)]+\)")
# [foo|bar|baz]
_either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]")
_camel_re = re.compile(r"([A-Z]+)([a-z])")
_cache = {} # type: Dict[URLPattern, str]
def _simplify(self, pattern):
# type: (str) -> str
r"""
Clean up urlpattern regexes into something readable by humans:
From:
> "^(?P<sport_slug>\w+)/athletes/(?P<athlete_slug>\w+)/$"
To:
> "{sport_slug}/athletes/{athlete_slug}/"
"""
# remove optional params
# TODO(dcramer): it'd be nice to change these into [%s] but it currently
# conflicts with the other rules because we're doing regexp matches
# rather than parsing tokens
result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1), pattern)
# handle named groups first
result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result)
# handle non-named groups
result = self._non_named_group_matcher.sub("{var}", result)
# handle optional params
result = self._either_option_matcher.sub(lambda m: m.group(1), result)
# clean up any outstanding regex-y characters.
result = (
result.replace("^", "")
.replace("$", "")
.replace("?", "")
.replace("//", "/")
.replace("\\", "")
)
return result
def _resolve(self, resolver, path, parents=None):
# type: (URLResolver, str, Optional[List[URLResolver]]) -> Optional[str]
match = get_regex(resolver).search(path) # Django < 2.0
if not match:
return None
if parents is None:
parents = [resolver]
elif resolver not in parents:
parents = parents + [resolver]
new_path = path[match.end() :]
for pattern in resolver.url_patterns:
# this is an include()
if not pattern.callback:
match = self._resolve(pattern, new_path, parents)
if match:
return match
continue
elif not get_regex(pattern).search(new_path):
continue
try:
return self._cache[pattern]
except KeyError:
pass
prefix = "".join(self._simplify(get_regex(p).pattern) for p in parents)
result = prefix + self._simplify(get_regex(pattern).pattern)
if not result.startswith("/"):
result = "/" + result
self._cache[pattern] = result
return result
return None
def resolve(
self,
path, # type: str
urlconf=None, # type: Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]]
):
# type: (...) -> str
resolver = get_resolver(urlconf)
match = self._resolve(resolver, path)
return match or path
LEGACY_RESOLVER = RavenResolver()

View File

@ -0,0 +1,68 @@
import sys
from sentry_sdk.hub import Hub
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
from sentry_sdk.integrations import Integration
from sentry_sdk._types import MYPY
if MYPY:
from typing import Callable
from typing import Any
class ExcepthookIntegration(Integration):
identifier = "excepthook"
always_run = False
def __init__(self, always_run=False):
# type: (bool) -> None
if not isinstance(always_run, bool):
raise ValueError(
"Invalid value for always_run: %s (must be type boolean)"
% (always_run,)
)
self.always_run = always_run
@staticmethod
def setup_once():
# type: () -> None
sys.excepthook = _make_excepthook(sys.excepthook)
def _make_excepthook(old_excepthook):
# type: (Callable) -> Callable
def sentry_sdk_excepthook(exctype, value, traceback):
hub = Hub.current
integration = hub.get_integration(ExcepthookIntegration)
if integration is not None and _should_send(integration.always_run):
# If an integration is there, a client has to be there.
client = hub.client # type: Any
with capture_internal_exceptions():
event, hint = event_from_exception(
(exctype, value, traceback),
client_options=client.options,
mechanism={"type": "excepthook", "handled": False},
)
hub.capture_event(event, hint=hint)
return old_excepthook(exctype, value, traceback)
return sentry_sdk_excepthook
def _should_send(always_run=False):
# type: (bool) -> bool
if always_run:
return True
if hasattr(sys, "ps1"):
# Disable the excepthook for interactive Python shells, otherwise
# every typo gets sent to Sentry.
return False
return True

View File

@ -0,0 +1,176 @@
from __future__ import absolute_import
import falcon # type: ignore
import falcon.api_helpers # type: ignore
from sentry_sdk.hub import Hub
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
from typing import Callable
from typing import Dict
class FalconRequestExtractor(RequestExtractor):
def env(self):
return self.request.env
def cookies(self):
return self.request.cookies
def form(self):
return None # No such concept in Falcon
def files(self):
return None # No such concept in Falcon
def raw_data(self):
# As request data can only be read once we won't make this available
# to Sentry. Just send back a dummy string in case there was a
# content length.
# TODO(jmagnusson): Figure out if there's a way to support this
content_length = self.content_length()
if content_length > 0:
return "[REQUEST_CONTAINING_RAW_DATA]"
else:
return None
def json(self):
try:
return self.request.media
except falcon.errors.HTTPBadRequest:
# NOTE(jmagnusson): We return `falcon.Request._media` here because
# falcon 1.4 doesn't do proper type checking in
# `falcon.Request.media`. This has been fixed in 2.0.
# Relevant code: https://github.com/falconry/falcon/blob/1.4.1/falcon/request.py#L953
return self.request._media
class SentryFalconMiddleware(object):
"""Captures exceptions in Falcon requests and send to Sentry"""
def process_request(self, req, resp, *args, **kwargs):
hub = Hub.current
integration = hub.get_integration(FalconIntegration)
if integration is None:
return
with hub.configure_scope() as scope:
scope._name = "falcon"
scope.add_event_processor(_make_request_event_processor(req, integration))
class FalconIntegration(Integration):
identifier = "falcon"
transaction_style = None
def __init__(self, transaction_style="uri_template"):
# type: (str) -> None
TRANSACTION_STYLE_VALUES = ("uri_template", "path")
if transaction_style not in TRANSACTION_STYLE_VALUES:
raise ValueError(
"Invalid value for transaction_style: %s (must be in %s)"
% (transaction_style, TRANSACTION_STYLE_VALUES)
)
self.transaction_style = transaction_style
@staticmethod
def setup_once():
# type: () -> None
_patch_wsgi_app()
_patch_handle_exception()
_patch_prepare_middleware()
def _patch_wsgi_app():
original_wsgi_app = falcon.API.__call__
def sentry_patched_wsgi_app(self, env, start_response):
hub = Hub.current
integration = hub.get_integration(FalconIntegration)
if integration is None:
return original_wsgi_app(self, env, start_response)
sentry_wrapped = SentryWsgiMiddleware(
lambda envi, start_resp: original_wsgi_app(self, envi, start_resp)
)
return sentry_wrapped(env, start_response)
falcon.API.__call__ = sentry_patched_wsgi_app
def _patch_handle_exception():
original_handle_exception = falcon.API._handle_exception
def sentry_patched_handle_exception(self, *args):
# NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception
# method signature from `(ex, req, resp, params)` to
# `(req, resp, ex, params)`
if isinstance(args[0], Exception):
ex = args[0]
else:
ex = args[2]
was_handled = original_handle_exception(self, *args)
hub = Hub.current
integration = hub.get_integration(FalconIntegration)
if integration is not None and not _is_falcon_http_error(ex):
# If an integration is there, a client has to be there.
client = hub.client # type: Any
event, hint = event_from_exception(
ex,
client_options=client.options,
mechanism={"type": "falcon", "handled": False},
)
hub.capture_event(event, hint=hint)
return was_handled
falcon.API._handle_exception = sentry_patched_handle_exception
def _patch_prepare_middleware():
original_prepare_middleware = falcon.api_helpers.prepare_middleware
def sentry_patched_prepare_middleware(
middleware=None, independent_middleware=False
):
hub = Hub.current
integration = hub.get_integration(FalconIntegration)
if integration is not None:
middleware = [SentryFalconMiddleware()] + (middleware or [])
return original_prepare_middleware(middleware, independent_middleware)
falcon.api_helpers.prepare_middleware = sentry_patched_prepare_middleware
def _is_falcon_http_error(ex):
return isinstance(ex, (falcon.HTTPError, falcon.http_status.HTTPStatus))
def _make_request_event_processor(req, integration):
# type: (falcon.Request, FalconIntegration) -> Callable
def inner(event, hint):
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
if integration.transaction_style == "uri_template":
event["transaction"] = req.uri_template
elif integration.transaction_style == "path":
event["transaction"] = req.path
with capture_internal_exceptions():
FalconRequestExtractor(req).extract_into_event(event)
return event
return inner

View File

@ -0,0 +1,237 @@
from __future__ import absolute_import
import weakref
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk._types import MYPY
if MYPY:
from sentry_sdk.integrations.wsgi import _ScopedResponse
from typing import Any
from typing import Dict
from werkzeug.datastructures import ImmutableTypeConversionDict
from werkzeug.datastructures import ImmutableMultiDict
from werkzeug.datastructures import FileStorage
from typing import Union
from typing import Callable
try:
import flask_login # type: ignore
except ImportError:
flask_login = None
from flask import Request, Flask, _request_ctx_stack, _app_ctx_stack # type: ignore
from flask.signals import (
appcontext_pushed,
appcontext_tearing_down,
got_request_exception,
request_started,
)
class FlaskIntegration(Integration):
identifier = "flask"
transaction_style = None
def __init__(self, transaction_style="endpoint"):
# type: (str) -> None
TRANSACTION_STYLE_VALUES = ("endpoint", "url")
if transaction_style not in TRANSACTION_STYLE_VALUES:
raise ValueError(
"Invalid value for transaction_style: %s (must be in %s)"
% (transaction_style, TRANSACTION_STYLE_VALUES)
)
self.transaction_style = transaction_style
@staticmethod
def setup_once():
# type: () -> None
appcontext_pushed.connect(_push_appctx)
appcontext_tearing_down.connect(_pop_appctx)
request_started.connect(_request_started)
got_request_exception.connect(_capture_exception)
old_app = Flask.__call__
def sentry_patched_wsgi_app(self, environ, start_response):
# type: (Any, Dict[str, str], Callable) -> _ScopedResponse
if Hub.current.get_integration(FlaskIntegration) is None:
return old_app(self, environ, start_response)
return SentryWsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))(
environ, start_response
)
Flask.__call__ = sentry_patched_wsgi_app # type: ignore
def _push_appctx(*args, **kwargs):
# type: (*Flask, **Any) -> None
hub = Hub.current
if hub.get_integration(FlaskIntegration) is not None:
# always want to push scope regardless of whether WSGI app might already
# have (not the case for CLI for example)
scope_manager = hub.push_scope()
scope_manager.__enter__()
_app_ctx_stack.top.sentry_sdk_scope_manager = scope_manager
with hub.configure_scope() as scope:
scope._name = "flask"
def _pop_appctx(*args, **kwargs):
# type: (*Flask, **Any) -> None
scope_manager = getattr(_app_ctx_stack.top, "sentry_sdk_scope_manager", None)
if scope_manager is not None:
scope_manager.__exit__(None, None, None)
def _request_started(sender, **kwargs):
# type: (Flask, **Any) -> None
hub = Hub.current
integration = hub.get_integration(FlaskIntegration)
if integration is None:
return
app = _app_ctx_stack.top.app
with hub.configure_scope() as scope:
request = _request_ctx_stack.top.request
# Rely on WSGI middleware to start a trace
try:
if integration.transaction_style == "endpoint":
scope.transaction = request.url_rule.endpoint # type: ignore
elif integration.transaction_style == "url":
scope.transaction = request.url_rule.rule # type: ignore
except Exception:
pass
weak_request = weakref.ref(request)
scope.add_event_processor(
_make_request_event_processor( # type: ignore
app, weak_request, integration
)
)
class FlaskRequestExtractor(RequestExtractor):
def env(self):
# type: () -> Dict[str, str]
return self.request.environ
def cookies(self):
# type: () -> ImmutableTypeConversionDict
return self.request.cookies
def raw_data(self):
# type: () -> bytes
return self.request.get_data()
def form(self):
# type: () -> ImmutableMultiDict
return self.request.form
def files(self):
# type: () -> ImmutableMultiDict
return self.request.files
def is_json(self):
return self.request.is_json
def json(self):
return self.request.get_json()
def size_of_file(self, file):
# type: (FileStorage) -> int
return file.content_length
def _make_request_event_processor(app, weak_request, integration):
# type: (Flask, Callable[[], Request], FlaskIntegration) -> Callable
def inner(event, hint):
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
request = weak_request()
# if the request is gone we are fine not logging the data from
# it. This might happen if the processor is pushed away to
# another thread.
if request is None:
return event
with capture_internal_exceptions():
FlaskRequestExtractor(request).extract_into_event(event)
if _should_send_default_pii():
with capture_internal_exceptions():
_add_user_to_event(event)
return event
return inner
def _capture_exception(sender, exception, **kwargs):
# type: (Flask, Union[ValueError, BaseException], **Any) -> None
hub = Hub.current
if hub.get_integration(FlaskIntegration) is None:
return
# If an integration is there, a client has to be there.
client = hub.client # type: Any
event, hint = event_from_exception(
exception,
client_options=client.options,
mechanism={"type": "flask", "handled": False},
)
hub.capture_event(event, hint=hint)
def _add_user_to_event(event):
# type: (Dict[str, Any]) -> None
if flask_login is None:
return
user = flask_login.current_user
if user is None:
return
with capture_internal_exceptions():
# Access this object as late as possible as accessing the user
# is relatively costly
user_info = event.setdefault("user", {})
try:
user_info["id"] = user.get_id()
# TODO: more configurable user attrs here
except AttributeError:
# might happen if:
# - flask_login could not be imported
# - flask_login is not configured
# - no user is logged in
pass
# The following attribute accesses are ineffective for the general
# Flask-Login case, because the User interface of Flask-Login does not
# care about anything but the ID. However, Flask-User (based on
# Flask-Login) documents a few optional extra attributes.
#
# https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names
try:
user_info["email"] = user_info["username"] = user.email
except Exception:
pass
try:
user_info["username"] = user.username
except Exception:
pass

View File

@ -0,0 +1,106 @@
import re
from sentry_sdk.hub import Hub
from sentry_sdk.integrations import Integration
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk.utils import capture_internal_exceptions
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
from typing import Dict
MODULE_RE = r"[a-zA-Z0-9/._:\\-]+"
TYPE_RE = r"[a-zA-Z0-9._:<>,-]+"
HEXVAL_RE = r"[A-Fa-f0-9]+"
FRAME_RE = r"""
^(?P<index>\d+)\.\s
(?P<package>{MODULE_RE})\(
(?P<retval>{TYPE_RE}\ )?
((?P<function>{TYPE_RE})
(?P<args>\(.*\))?
)?
((?P<constoffset>\ const)?\+0x(?P<offset>{HEXVAL_RE}))?
\)\s
\[0x(?P<retaddr>{HEXVAL_RE})\]$
""".format(
MODULE_RE=MODULE_RE, HEXVAL_RE=HEXVAL_RE, TYPE_RE=TYPE_RE
)
FRAME_RE = re.compile(FRAME_RE, re.MULTILINE | re.VERBOSE)
class GnuBacktraceIntegration(Integration):
identifier = "gnu_backtrace"
@staticmethod
def setup_once():
# type: () -> None
@add_global_event_processor
def process_gnu_backtrace(event, hint):
with capture_internal_exceptions():
return _process_gnu_backtrace(event, hint)
def _process_gnu_backtrace(event, hint):
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
if Hub.current.get_integration(GnuBacktraceIntegration) is None:
return event
exc_info = hint.get("exc_info", None)
if exc_info is None:
return event
exception = event.get("exception", None)
if exception is None:
return event
values = exception.get("values", None)
if values is None:
return event
for exception in values:
frames = exception.get("stacktrace", {}).get("frames", [])
if not frames:
continue
msg = exception.get("value", None)
if not msg:
continue
additional_frames = []
new_msg = []
for line in msg.splitlines():
match = FRAME_RE.match(line)
if match:
additional_frames.append(
(
int(match.group("index")),
{
"package": match.group("package") or None,
"function": match.group("function") or None,
"platform": "native",
},
)
)
else:
# Put garbage lines back into message, not sure what to do with them.
new_msg.append(line)
if additional_frames:
additional_frames.sort(key=lambda x: -x[0])
for _, frame in additional_frames:
frames.append(frame)
new_msg.append("<stacktrace parsed and removed by GnuBacktraceIntegration>")
exception["value"] = "\n".join(new_msg)
return event

View File

@ -0,0 +1,236 @@
from __future__ import absolute_import
import logging
import datetime
from sentry_sdk.hub import Hub
from sentry_sdk.utils import (
to_string,
event_from_exception,
current_stacktrace,
capture_internal_exceptions,
)
from sentry_sdk.integrations import Integration
from sentry_sdk._compat import iteritems
from sentry_sdk._types import MYPY
if MYPY:
from logging import LogRecord
from typing import Any
from typing import Dict
from typing import Optional
DEFAULT_LEVEL = logging.INFO
DEFAULT_EVENT_LEVEL = logging.ERROR
_IGNORED_LOGGERS = set(["sentry_sdk.errors"])
def ignore_logger(
name # type: str
):
# type: (...) -> None
"""This disables recording (both in breadcrumbs and as events) calls to
a logger of a specific name. Among other uses, many of our integrations
use this to prevent their actions being recorded as breadcrumbs. Exposed
to users as a way to quiet spammy loggers.
:param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``).
"""
_IGNORED_LOGGERS.add(name)
class LoggingIntegration(Integration):
identifier = "logging"
def __init__(self, level=DEFAULT_LEVEL, event_level=DEFAULT_EVENT_LEVEL):
# type: (Optional[int], Optional[int]) -> None
self._handler = None
self._breadcrumb_handler = None
if level is not None:
self._breadcrumb_handler = BreadcrumbHandler(level=level)
if event_level is not None:
self._handler = EventHandler(level=event_level)
def _handle_record(self, record):
# type: (LogRecord) -> None
if self._handler is not None and record.levelno >= self._handler.level:
self._handler.handle(record)
if (
self._breadcrumb_handler is not None
and record.levelno >= self._breadcrumb_handler.level
):
self._breadcrumb_handler.handle(record)
@staticmethod
def setup_once():
# type: () -> None
old_callhandlers = logging.Logger.callHandlers # type: ignore
def sentry_patched_callhandlers(self, record):
# type: (Any, LogRecord) -> Any
try:
return old_callhandlers(self, record)
finally:
# This check is done twice, once also here before we even get
# the integration. Otherwise we have a high chance of getting
# into a recursion error when the integration is resolved
# (this also is slower).
if record.name not in _IGNORED_LOGGERS:
integration = Hub.current.get_integration(LoggingIntegration)
if integration is not None:
integration._handle_record(record)
logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore
def _can_record(record):
# type: (LogRecord) -> bool
return record.name not in _IGNORED_LOGGERS
def _breadcrumb_from_record(record):
# type: (LogRecord) -> Dict[str, Any]
return {
"ty": "log",
"level": _logging_to_event_level(record.levelname),
"category": record.name,
"message": record.message,
"timestamp": datetime.datetime.utcfromtimestamp(record.created),
"data": _extra_from_record(record),
}
def _logging_to_event_level(levelname):
# type: (str) -> str
return {"critical": "fatal"}.get(levelname.lower(), levelname.lower())
COMMON_RECORD_ATTRS = frozenset(
(
"args",
"created",
"exc_info",
"exc_text",
"filename",
"funcName",
"levelname",
"levelno",
"linenno",
"lineno",
"message",
"module",
"msecs",
"msg",
"name",
"pathname",
"process",
"processName",
"relativeCreated",
"stack",
"tags",
"thread",
"threadName",
)
)
def _extra_from_record(record):
# type: (LogRecord) -> Dict[str, None]
return {
k: v
for k, v in iteritems(vars(record))
if k not in COMMON_RECORD_ATTRS
and (not isinstance(k, str) or not k.startswith("_"))
}
class EventHandler(logging.Handler, object):
"""
A logging handler that emits Sentry events for each log record
Note that you do not have to use this class if the logging integration is enabled, which it is by default.
"""
def emit(self, record):
# type: (LogRecord) -> Any
with capture_internal_exceptions():
self.format(record)
return self._emit(record)
def _emit(self, record):
# type: (LogRecord) -> None
if not _can_record(record):
return
hub = Hub.current
if hub.client is None:
return
client_options = hub.client.options
# exc_info might be None or (None, None, None)
if record.exc_info is not None and record.exc_info[0] is not None:
event, hint = event_from_exception(
record.exc_info,
client_options=client_options,
mechanism={"type": "logging", "handled": True},
)
elif record.exc_info and record.exc_info[0] is None:
event = {}
hint = {}
with capture_internal_exceptions():
event["threads"] = {
"values": [
{
"stacktrace": current_stacktrace(
client_options["with_locals"]
),
"crashed": False,
"current": True,
}
]
}
else:
event = {}
hint = {}
hint["log_record"] = record
event["level"] = _logging_to_event_level(record.levelname)
event["logger"] = record.name
event["logentry"] = {"message": to_string(record.msg), "params": record.args}
event["extra"] = _extra_from_record(record)
hub.capture_event(event, hint=hint)
# Legacy name
SentryHandler = EventHandler
class BreadcrumbHandler(logging.Handler, object):
"""
A logging handler that records breadcrumbs for each log record.
Note that you do not have to use this class if the logging integration is enabled, which it is by default.
"""
def emit(self, record):
# type: (LogRecord) -> Any
with capture_internal_exceptions():
self.format(record)
return self._emit(record)
def _emit(self, record):
# type: (LogRecord) -> None
if not _can_record(record):
return
Hub.current.add_breadcrumb(
_breadcrumb_from_record(record), hint={"log_record": record}
)

View File

@ -0,0 +1,56 @@
from __future__ import absolute_import
from sentry_sdk.hub import Hub
from sentry_sdk.integrations import Integration
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
from typing import Dict
from typing import Tuple
from typing import Iterator
from sentry_sdk._types import Event
_installed_modules = None
def _generate_installed_modules():
# type: () -> Iterator[Tuple[str, str]]
try:
import pkg_resources
except ImportError:
return
for info in pkg_resources.working_set:
yield info.key, info.version
def _get_installed_modules():
# type: () -> Dict[str, str]
global _installed_modules
if _installed_modules is None:
_installed_modules = dict(_generate_installed_modules())
return _installed_modules
class ModulesIntegration(Integration):
identifier = "modules"
@staticmethod
def setup_once():
# type: () -> None
@add_global_event_processor
def processor(event, hint):
# type: (Event, Any) -> Dict[str, Any]
if event.get("type") == "transaction":
return event
if Hub.current.get_integration(ModulesIntegration) is None:
return event
event["modules"] = dict(_get_installed_modules())
return event

View File

@ -0,0 +1,211 @@
from __future__ import absolute_import
import os
import sys
import weakref
from pyramid.httpexceptions import HTTPException # type: ignore
from pyramid.request import Request # type: ignore
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
from sentry_sdk._compat import reraise, iteritems
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk._types import MYPY
if MYPY:
from pyramid.response import Response # type: ignore
from typing import Any
from sentry_sdk.integrations.wsgi import _ScopedResponse
from typing import Callable
from typing import Dict
from typing import Optional
from webob.cookies import RequestCookies # type: ignore
from webob.compat import cgi_FieldStorage # type: ignore
from sentry_sdk.utils import ExcInfo
if getattr(Request, "authenticated_userid", None):
def authenticated_userid(request):
# type: (Request) -> Optional[Any]
return request.authenticated_userid
else:
# bw-compat for pyramid < 1.5
from pyramid.security import authenticated_userid # type: ignore
class PyramidIntegration(Integration):
identifier = "pyramid"
transaction_style = None
def __init__(self, transaction_style="route_name"):
# type: (str) -> None
TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern")
if transaction_style not in TRANSACTION_STYLE_VALUES:
raise ValueError(
"Invalid value for transaction_style: %s (must be in %s)"
% (transaction_style, TRANSACTION_STYLE_VALUES)
)
self.transaction_style = transaction_style
@staticmethod
def setup_once():
# type: () -> None
from pyramid.router import Router # type: ignore
from pyramid.request import Request # type: ignore
old_handle_request = Router.handle_request
def sentry_patched_handle_request(self, request, *args, **kwargs):
# type: (Any, Request, *Any, **Any) -> Response
hub = Hub.current
integration = hub.get_integration(PyramidIntegration)
if integration is not None:
with hub.configure_scope() as scope:
scope.add_event_processor(
_make_event_processor(weakref.ref(request), integration)
)
return old_handle_request(self, request, *args, **kwargs)
Router.handle_request = sentry_patched_handle_request
if hasattr(Request, "invoke_exception_view"):
old_invoke_exception_view = Request.invoke_exception_view
def sentry_patched_invoke_exception_view(self, *args, **kwargs):
rv = old_invoke_exception_view(self, *args, **kwargs)
if (
self.exc_info
and all(self.exc_info)
and rv.status_int == 500
and Hub.current.get_integration(PyramidIntegration) is not None
):
_capture_exception(self.exc_info)
return rv
Request.invoke_exception_view = sentry_patched_invoke_exception_view
old_wsgi_call = Router.__call__
def sentry_patched_wsgi_call(self, environ, start_response):
# type: (Any, Dict[str, str], Callable) -> _ScopedResponse
hub = Hub.current
integration = hub.get_integration(PyramidIntegration)
if integration is None:
return old_wsgi_call(self, environ, start_response)
def sentry_patched_inner_wsgi_call(environ, start_response):
try:
return old_wsgi_call(self, environ, start_response)
except Exception:
einfo = sys.exc_info()
_capture_exception(einfo)
reraise(*einfo)
return SentryWsgiMiddleware(sentry_patched_inner_wsgi_call)(
environ, start_response
)
Router.__call__ = sentry_patched_wsgi_call
def _capture_exception(exc_info):
# type: (ExcInfo) -> None
if exc_info[0] is None or issubclass(exc_info[0], HTTPException):
return
hub = Hub.current
if hub.get_integration(PyramidIntegration) is None:
return
# If an integration is there, a client has to be there.
client = hub.client # type: Any
event, hint = event_from_exception(
exc_info,
client_options=client.options,
mechanism={"type": "pyramid", "handled": False},
)
hub.capture_event(event, hint=hint)
class PyramidRequestExtractor(RequestExtractor):
def url(self):
return self.request.path_url
def env(self):
# type: () -> Dict[str, str]
return self.request.environ
def cookies(self):
# type: () -> RequestCookies
return self.request.cookies
def raw_data(self):
# type: () -> str
return self.request.text
def form(self):
# type: () -> Dict[str, str]
return {
key: value
for key, value in iteritems(self.request.POST)
if not getattr(value, "filename", None)
}
def files(self):
# type: () -> Dict[str, cgi_FieldStorage]
return {
key: value
for key, value in iteritems(self.request.POST)
if getattr(value, "filename", None)
}
def size_of_file(self, postdata):
# type: (cgi_FieldStorage) -> int
file = postdata.file
try:
return os.fstat(file.fileno()).st_size
except Exception:
return 0
def _make_event_processor(weak_request, integration):
# type: (Callable[[], Request], PyramidIntegration) -> Callable
def event_processor(event, hint):
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
request = weak_request()
if request is None:
return event
try:
if integration.transaction_style == "route_name":
event["transaction"] = request.matched_route.name
elif integration.transaction_style == "route_pattern":
event["transaction"] = request.matched_route.pattern
except Exception:
pass
with capture_internal_exceptions():
PyramidRequestExtractor(request).extract_into_event(event)
if _should_send_default_pii():
with capture_internal_exceptions():
user_info = event.setdefault("user", {})
user_info["id"] = authenticated_userid(request)
return event
return event_processor

View File

@ -0,0 +1,46 @@
from __future__ import absolute_import
from sentry_sdk import Hub
from sentry_sdk.utils import capture_internal_exceptions
from sentry_sdk.integrations import Integration
class RedisIntegration(Integration):
identifier = "redis"
@staticmethod
def setup_once():
import redis
old_execute_command = redis.StrictRedis.execute_command
def sentry_patched_execute_command(self, name, *args, **kwargs):
hub = Hub.current
if hub.get_integration(RedisIntegration) is None:
return old_execute_command(self, name, *args, **kwargs)
description = name
with capture_internal_exceptions():
description_parts = [name]
for i, arg in enumerate(args):
if i > 10:
break
description_parts.append(repr(arg))
description = " ".join(description_parts)
with hub.start_span(op="redis", description=description) as span:
if name:
span.set_tag("redis.command", name)
if name and args and name.lower() in ("get", "set", "setex", "setnx"):
span.set_tag("redis.key", args[0])
return old_execute_command(self, name, *args, **kwargs)
redis.StrictRedis.execute_command = ( # type: ignore
sentry_patched_execute_command
)

View File

@ -0,0 +1,134 @@
from __future__ import absolute_import
import weakref
from sentry_sdk.hub import Hub
from sentry_sdk.integrations import Integration
from sentry_sdk.tracing import Span
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
from rq.timeouts import JobTimeoutException # type: ignore
from rq.worker import Worker # type: ignore
from rq.queue import Queue # type: ignore
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
from typing import Dict
from typing import Callable
from rq.job import Job # type: ignore
from sentry_sdk.utils import ExcInfo
class RqIntegration(Integration):
identifier = "rq"
@staticmethod
def setup_once():
# type: () -> None
old_perform_job = Worker.perform_job
def sentry_patched_perform_job(self, job, *args, **kwargs):
# type: (Any, Job, *Queue, **Any) -> bool
hub = Hub.current
integration = hub.get_integration(RqIntegration)
if integration is None:
return old_perform_job(self, job, *args, **kwargs)
client = hub.client
assert client is not None
with hub.push_scope() as scope:
scope.clear_breadcrumbs()
scope.add_event_processor(_make_event_processor(weakref.ref(job)))
span = Span.continue_from_headers(
job.meta.get("_sentry_trace_headers") or {}
)
span.op = "rq.task"
with capture_internal_exceptions():
span.transaction = job.func_name
with hub.start_span(span):
rv = old_perform_job(self, job, *args, **kwargs)
if self.is_horse:
# We're inside of a forked process and RQ is
# about to call `os._exit`. Make sure that our
# events get sent out.
client.flush()
return rv
Worker.perform_job = sentry_patched_perform_job
old_handle_exception = Worker.handle_exception
def sentry_patched_handle_exception(self, job, *exc_info, **kwargs):
_capture_exception(exc_info) # type: ignore
return old_handle_exception(self, job, *exc_info, **kwargs)
Worker.handle_exception = sentry_patched_handle_exception
old_enqueue_job = Queue.enqueue_job
def sentry_patched_enqueue_job(self, job, **kwargs):
hub = Hub.current
if hub.get_integration(RqIntegration) is not None:
job.meta["_sentry_trace_headers"] = dict(
hub.iter_trace_propagation_headers()
)
return old_enqueue_job(self, job, **kwargs)
Queue.enqueue_job = sentry_patched_enqueue_job
def _make_event_processor(weak_job):
# type: (Callable[[], Job]) -> Callable
def event_processor(event, hint):
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
job = weak_job()
if job is not None:
with capture_internal_exceptions():
extra = event.setdefault("extra", {})
extra["rq-job"] = {
"job_id": job.id,
"func": job.func_name,
"args": job.args,
"kwargs": job.kwargs,
"description": job.description,
}
if "exc_info" in hint:
with capture_internal_exceptions():
if issubclass(hint["exc_info"][0], JobTimeoutException):
event["fingerprint"] = ["rq", "JobTimeoutException", job.func_name]
return event
return event_processor
def _capture_exception(exc_info, **kwargs):
# type: (ExcInfo, **Any) -> None
hub = Hub.current
if hub.get_integration(RqIntegration) is None:
return
# If an integration is there, a client has to be there.
client = hub.client # type: Any
event, hint = event_from_exception(
exc_info,
client_options=client.options,
mechanism={"type": "rq", "handled": False},
)
hub.capture_event(event, hint=hint)

View File

@ -0,0 +1,218 @@
import sys
import weakref
from inspect import isawaitable
from sentry_sdk._compat import urlparse, reraise
from sentry_sdk.hub import Hub
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
HAS_REAL_CONTEXTVARS,
)
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers
from sentry_sdk.integrations.logging import ignore_logger
from sanic import Sanic, __version__ as VERSION # type: ignore
from sanic.exceptions import SanicException # type: ignore
from sanic.router import Router # type: ignore
from sanic.handlers import ErrorHandler # type: ignore
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
from typing import Callable
from typing import Optional
from typing import Union
from typing import Tuple
from sanic.request import Request, RequestParameters # type: ignore
from sentry_sdk._types import Event, EventProcessor, Hint
class SanicIntegration(Integration):
identifier = "sanic"
@staticmethod
def setup_once():
# type: () -> None
if not HAS_REAL_CONTEXTVARS:
# We better have contextvars or we're going to leak state between
# requests.
raise RuntimeError(
"The sanic integration for Sentry requires Python 3.7+ "
" or aiocontextvars package"
)
if VERSION.startswith("0.8."):
# Sanic 0.8 and older creates a logger named "root" and puts a
# stringified version of every exception in there (without exc_info),
# which our error deduplication can't detect.
#
# We explicitly check the version here because it is a very
# invasive step to ignore this logger and not necessary in newer
# versions at all.
#
# https://github.com/huge-success/sanic/issues/1332
ignore_logger("root")
old_handle_request = Sanic.handle_request
async def sentry_handle_request(self, request, *args, **kwargs):
# type: (Any, Request, *Any, **Any) -> Any
hub = Hub.current
if hub.get_integration(SanicIntegration) is None:
return old_handle_request(self, request, *args, **kwargs)
weak_request = weakref.ref(request)
with Hub(hub) as hub:
with hub.configure_scope() as scope:
scope.clear_breadcrumbs()
scope.add_event_processor(_make_request_processor(weak_request))
response = old_handle_request(self, request, *args, **kwargs)
if isawaitable(response):
response = await response
return response
Sanic.handle_request = sentry_handle_request
old_router_get = Router.get
def sentry_router_get(self, request):
# type: (Any, Request) -> Any
rv = old_router_get(self, request)
hub = Hub.current
if hub.get_integration(SanicIntegration) is not None:
with capture_internal_exceptions():
with hub.configure_scope() as scope:
scope.transaction = rv[0].__name__
return rv
Router.get = sentry_router_get
old_error_handler_lookup = ErrorHandler.lookup
def sentry_error_handler_lookup(self, exception):
# type: (Any, Exception) -> Optional[Callable]
_capture_exception(exception)
old_error_handler = old_error_handler_lookup(self, exception)
if old_error_handler is None:
return None
if Hub.current.get_integration(SanicIntegration) is None:
return old_error_handler
async def sentry_wrapped_error_handler(request, exception):
# type: (Request, Exception) -> Any
try:
response = old_error_handler(request, exception)
if isawaitable(response):
response = await response
return response
except Exception:
# Report errors that occur in Sanic error handler. These
# exceptions will not even show up in Sanic's
# `sanic.exceptions` logger.
exc_info = sys.exc_info()
_capture_exception(exc_info)
reraise(*exc_info)
return sentry_wrapped_error_handler
ErrorHandler.lookup = sentry_error_handler_lookup
def _capture_exception(exception):
# type: (Union[Tuple[Optional[type], Optional[BaseException], Any], BaseException]) -> None
hub = Hub.current
integration = hub.get_integration(SanicIntegration)
if integration is None:
return
# If an integration is there, a client has to be there.
client = hub.client # type: Any
with capture_internal_exceptions():
event, hint = event_from_exception(
exception,
client_options=client.options,
mechanism={"type": "sanic", "handled": False},
)
hub.capture_event(event, hint=hint)
def _make_request_processor(weak_request):
# type: (Callable[[], Request]) -> EventProcessor
def sanic_processor(event, hint):
# type: (Event, Optional[Hint]) -> Optional[Event]
try:
if hint and issubclass(hint["exc_info"][0], SanicException):
return None
except KeyError:
pass
request = weak_request()
if request is None:
return event
with capture_internal_exceptions():
extractor = SanicRequestExtractor(request)
extractor.extract_into_event(event)
request_info = event["request"]
urlparts = urlparse.urlsplit(request.url)
request_info["url"] = "%s://%s%s" % (
urlparts.scheme,
urlparts.netloc,
urlparts.path,
)
request_info["query_string"] = urlparts.query
request_info["method"] = request.method
request_info["env"] = {"REMOTE_ADDR": request.remote_addr}
request_info["headers"] = _filter_headers(dict(request.headers))
return event
return sanic_processor
class SanicRequestExtractor(RequestExtractor):
def content_length(self):
# type: () -> int
if self.request.body is None:
return 0
return len(self.request.body)
def cookies(self):
return dict(self.request.cookies)
def raw_data(self):
# type: () -> bytes
return self.request.body
def form(self):
# type: () -> RequestParameters
return self.request.form
def is_json(self):
raise NotImplementedError()
def json(self):
# type: () -> Optional[Any]
return self.request.json
def files(self):
# type: () -> RequestParameters
return self.request.files
def size_of_file(self, file):
return len(file.body or ())

View File

@ -0,0 +1,50 @@
import functools
import sys
from sentry_sdk.hub import Hub
from sentry_sdk.utils import event_from_exception
from sentry_sdk._compat import reraise
def serverless_function(f=None, flush=True):
def wrapper(f):
@functools.wraps(f)
def inner(*args, **kwargs):
with Hub(Hub.current) as hub:
with hub.configure_scope() as scope:
scope.clear_breadcrumbs()
try:
return f(*args, **kwargs)
except Exception:
_capture_and_reraise()
finally:
if flush:
_flush_client()
return inner
if f is None:
return wrapper
else:
return wrapper(f)
def _capture_and_reraise():
exc_info = sys.exc_info()
hub = Hub.current
if hub is not None and hub.client is not None:
event, hint = event_from_exception(
exc_info,
client_options=hub.client.options,
mechanism={"type": "serverless", "handled": False},
)
hub.capture_event(event, hint=hint)
reraise(*exc_info)
def _flush_client():
hub = Hub.current
if hub is not None:
hub.flush()

View File

@ -0,0 +1,69 @@
from __future__ import absolute_import
from sentry_sdk._types import MYPY
from sentry_sdk.hub import Hub
from sentry_sdk.integrations import Integration
from sentry_sdk.tracing import record_sql_queries
from sqlalchemy.engine import Engine # type: ignore
from sqlalchemy.event import listen # type: ignore
if MYPY:
from typing import Any
from typing import ContextManager
from typing import Optional
from sentry_sdk.tracing import Span
class SqlalchemyIntegration(Integration):
identifier = "sqlalchemy"
@staticmethod
def setup_once():
# type: () -> None
listen(Engine, "before_cursor_execute", _before_cursor_execute)
listen(Engine, "after_cursor_execute", _after_cursor_execute)
listen(Engine, "dbapi_error", _dbapi_error)
def _before_cursor_execute(
conn, cursor, statement, parameters, context, executemany, *args
):
# type: (Any, Any, Any, Any, Any, bool, *Any) -> None
hub = Hub.current
if hub.get_integration(SqlalchemyIntegration) is None:
return
ctx_mgr = record_sql_queries(
hub,
cursor,
statement,
parameters,
paramstyle=context and context.dialect and context.dialect.paramstyle or None,
executemany=executemany,
)
conn._sentry_sql_span_manager = ctx_mgr
span = ctx_mgr.__enter__()
if span is not None:
conn._sentry_sql_span = span
def _after_cursor_execute(conn, cursor, statement, *args):
# type: (Any, Any, Any, *Any) -> None
ctx_mgr = getattr(conn, "_sentry_sql_span_manager", None) # type: ContextManager
if ctx_mgr is not None:
conn._sentry_sql_span_manager = None
ctx_mgr.__exit__(None, None, None)
def _dbapi_error(conn, *args):
# type: (Any, *Any) -> None
span = getattr(conn, "_sentry_sql_span", None) # type: Optional[Span]
if span is not None:
span.set_failure()

View File

@ -0,0 +1,224 @@
import os
import subprocess
import sys
import platform
from sentry_sdk.hub import Hub
from sentry_sdk.integrations import Integration
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk.tracing import EnvironHeaders, record_http_request
from sentry_sdk.utils import capture_internal_exceptions, safe_repr
try:
from httplib import HTTPConnection # type: ignore
except ImportError:
from http.client import HTTPConnection
_RUNTIME_CONTEXT = {
"name": platform.python_implementation(),
"version": "%s.%s.%s" % (sys.version_info[:3]),
"build": sys.version,
}
class StdlibIntegration(Integration):
identifier = "stdlib"
@staticmethod
def setup_once():
# type: () -> None
_install_httplib()
_install_subprocess()
@add_global_event_processor
def add_python_runtime_context(event, hint):
if Hub.current.get_integration(StdlibIntegration) is not None:
contexts = event.setdefault("contexts", {})
if isinstance(contexts, dict) and "runtime" not in contexts:
contexts["runtime"] = _RUNTIME_CONTEXT
return event
def _install_httplib():
# type: () -> None
real_putrequest = HTTPConnection.putrequest
real_getresponse = HTTPConnection.getresponse
def putrequest(self, method, url, *args, **kwargs):
hub = Hub.current
if hub.get_integration(StdlibIntegration) is None:
return real_putrequest(self, method, url, *args, **kwargs)
host = self.host
port = self.port
default_port = self.default_port
real_url = url
if not real_url.startswith(("http://", "https://")):
real_url = "%s://%s%s%s" % (
default_port == 443 and "https" or "http",
host,
port != default_port and ":%s" % port or "",
url,
)
recorder = record_http_request(hub, real_url, method)
data_dict = recorder.__enter__()
try:
rv = real_putrequest(self, method, url, *args, **kwargs)
for key, value in hub.iter_trace_propagation_headers():
self.putheader(key, value)
except Exception:
recorder.__exit__(*sys.exc_info())
raise
self._sentrysdk_recorder = recorder
self._sentrysdk_data_dict = data_dict
return rv
def getresponse(self, *args, **kwargs):
recorder = getattr(self, "_sentrysdk_recorder", None)
if recorder is None:
return real_getresponse(self, *args, **kwargs)
data_dict = getattr(self, "_sentrysdk_data_dict", None)
try:
rv = real_getresponse(self, *args, **kwargs)
if data_dict is not None:
data_dict["httplib_response"] = rv
data_dict["status_code"] = rv.status
data_dict["reason"] = rv.reason
except TypeError:
# python-requests provokes a typeerror to discover py3 vs py2 differences
#
# > TypeError("getresponse() got an unexpected keyword argument 'buffering'")
raise
except Exception:
recorder.__exit__(*sys.exc_info())
raise
else:
recorder.__exit__(None, None, None)
return rv
HTTPConnection.putrequest = putrequest
HTTPConnection.getresponse = getresponse
def _init_argument(args, kwargs, name, position, setdefault_callback=None):
"""
given (*args, **kwargs) of a function call, retrieve (and optionally set a
default for) an argument by either name or position.
This is useful for wrapping functions with complex type signatures and
extracting a few arguments without needing to redefine that function's
entire type signature.
"""
if name in kwargs:
rv = kwargs[name]
if setdefault_callback is not None:
rv = setdefault_callback(rv)
if rv is not None:
kwargs[name] = rv
elif position < len(args):
rv = args[position]
if setdefault_callback is not None:
rv = setdefault_callback(rv)
if rv is not None:
args[position] = rv
else:
rv = setdefault_callback and setdefault_callback(None)
if rv is not None:
kwargs[name] = rv
return rv
def _install_subprocess():
old_popen_init = subprocess.Popen.__init__
def sentry_patched_popen_init(self, *a, **kw):
hub = Hub.current
if hub.get_integration(StdlibIntegration) is None:
return old_popen_init(self, *a, **kw)
# Convert from tuple to list to be able to set values.
a = list(a)
args = _init_argument(a, kw, "args", 0) or []
cwd = _init_argument(a, kw, "cwd", 9)
# if args is not a list or tuple (and e.g. some iterator instead),
# let's not use it at all. There are too many things that can go wrong
# when trying to collect an iterator into a list and setting that list
# into `a` again.
#
# Also invocations where `args` is not a sequence are not actually
# legal. They just happen to work under CPython.
description = None
if isinstance(args, (list, tuple)) and len(args) < 100:
with capture_internal_exceptions():
description = " ".join(map(str, args))
if description is None:
description = safe_repr(args)
env = None
for k, v in hub.iter_trace_propagation_headers():
if env is None:
env = _init_argument(a, kw, "env", 10, lambda x: dict(x or os.environ))
env["SUBPROCESS_" + k.upper().replace("-", "_")] = v
with hub.start_span(op="subprocess", description=description) as span:
span.set_data("subprocess.cwd", cwd)
rv = old_popen_init(self, *a, **kw)
span.set_tag("subprocess.pid", self.pid)
return rv
subprocess.Popen.__init__ = sentry_patched_popen_init # type: ignore
old_popen_wait = subprocess.Popen.wait
def sentry_patched_popen_wait(self, *a, **kw):
hub = Hub.current
if hub.get_integration(StdlibIntegration) is None:
return old_popen_wait(self, *a, **kw)
with hub.start_span(op="subprocess.wait") as span:
span.set_tag("subprocess.pid", self.pid)
return old_popen_wait(self, *a, **kw)
subprocess.Popen.wait = sentry_patched_popen_wait # type: ignore
old_popen_communicate = subprocess.Popen.communicate
def sentry_patched_popen_communicate(self, *a, **kw):
hub = Hub.current
if hub.get_integration(StdlibIntegration) is None:
return old_popen_communicate(self, *a, **kw)
with hub.start_span(op="subprocess.communicate") as span:
span.set_tag("subprocess.pid", self.pid)
return old_popen_communicate(self, *a, **kw)
subprocess.Popen.communicate = sentry_patched_popen_communicate # type: ignore
def get_subprocess_traceparent_headers():
return EnvironHeaders(os.environ, prefix="SUBPROCESS_")

View File

@ -0,0 +1,76 @@
from __future__ import absolute_import
import sys
from threading import Thread, current_thread
from sentry_sdk import Hub
from sentry_sdk._compat import reraise
from sentry_sdk._types import MYPY
from sentry_sdk.integrations import Integration
from sentry_sdk.utils import event_from_exception
if MYPY:
from typing import Any
class ThreadingIntegration(Integration):
identifier = "threading"
def __init__(self, propagate_hub=False):
self.propagate_hub = propagate_hub
@staticmethod
def setup_once():
# type: () -> None
old_start = Thread.start
def sentry_start(self, *a, **kw):
hub = Hub.current
integration = hub.get_integration(ThreadingIntegration)
if integration is not None:
if not integration.propagate_hub:
hub_ = None
else:
hub_ = Hub(hub)
# Patching instance methods in `start()` creates a reference cycle if
# done in a naive way. See
# https://github.com/getsentry/sentry-python/pull/434
#
# In threading module, using current_thread API will access current thread instance
# without holding it to avoid a reference cycle in an easier way.
self.run = _wrap_run(hub_, self.run.__func__)
return old_start(self, *a, **kw) # type: ignore
Thread.start = sentry_start # type: ignore
def _wrap_run(parent_hub, old_run_func):
def run(*a, **kw):
hub = parent_hub or Hub.current
with hub:
try:
self = current_thread()
return old_run_func(self, *a, **kw)
except Exception:
reraise(*_capture_exception())
return run
def _capture_exception():
hub = Hub.current
exc_info = sys.exc_info()
if hub.get_integration(ThreadingIntegration) is not None:
# If an integration is there, a client has to be there.
client = hub.client # type: Any
event, hint = event_from_exception(
exc_info,
client_options=client.options,
mechanism={"type": "threading", "handled": False},
)
hub.capture_event(event, hint=hint)
return exc_info

View File

@ -0,0 +1,197 @@
import weakref
from inspect import iscoroutinefunction
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.utils import (
HAS_REAL_CONTEXTVARS,
event_from_exception,
capture_internal_exceptions,
transaction_from_function,
)
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations._wsgi_common import (
RequestExtractor,
_filter_headers,
_is_json_content_type,
)
from sentry_sdk.integrations.logging import ignore_logger
from sentry_sdk._compat import iteritems
from tornado.web import RequestHandler, HTTPError # type: ignore
from tornado.gen import coroutine # type: ignore
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
from typing import List
from typing import Optional
from typing import Dict
from typing import Callable
class TornadoIntegration(Integration):
identifier = "tornado"
@staticmethod
def setup_once():
# type: () -> None
import tornado # type: ignore
tornado_version = getattr(tornado, "version_info", None)
if tornado_version is None or tornado_version < (5, 0):
raise RuntimeError("Tornado 5+ required")
if not HAS_REAL_CONTEXTVARS:
# Tornado is async. We better have contextvars or we're going to leak
# state between requests.
raise RuntimeError(
"The tornado integration for Sentry requires Python 3.6+ or the aiocontextvars package"
)
ignore_logger("tornado.application")
ignore_logger("tornado.access")
old_execute = RequestHandler._execute
awaitable = iscoroutinefunction(old_execute)
if awaitable:
# Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await)
# In that case our method should be a coroutine function too
async def sentry_execute_request_handler(self, *args, **kwargs):
# type: (Any, *List, **Any) -> Any
hub = Hub.current
integration = hub.get_integration(TornadoIntegration)
if integration is None:
return await old_execute(self, *args, **kwargs)
weak_handler = weakref.ref(self)
with Hub(hub) as hub:
with hub.configure_scope() as scope:
scope.clear_breadcrumbs()
scope.add_event_processor(_make_event_processor(weak_handler))
return await old_execute(self, *args, **kwargs)
else:
@coroutine # type: ignore
def sentry_execute_request_handler(self, *args, **kwargs):
hub = Hub.current
integration = hub.get_integration(TornadoIntegration)
if integration is None:
return old_execute(self, *args, **kwargs)
weak_handler = weakref.ref(self)
with Hub(hub) as hub:
with hub.configure_scope() as scope:
scope.add_event_processor(_make_event_processor(weak_handler))
result = yield from old_execute(self, *args, **kwargs)
return result
RequestHandler._execute = sentry_execute_request_handler
old_log_exception = RequestHandler.log_exception
def sentry_log_exception(self, ty, value, tb, *args, **kwargs):
# type: (Any, type, BaseException, Any, *Any, **Any) -> Optional[Any]
_capture_exception(ty, value, tb)
return old_log_exception(self, ty, value, tb, *args, **kwargs)
RequestHandler.log_exception = sentry_log_exception
def _capture_exception(ty, value, tb):
# type: (type, BaseException, Any) -> None
hub = Hub.current
if hub.get_integration(TornadoIntegration) is None:
return
if isinstance(value, HTTPError):
return
# If an integration is there, a client has to be there.
client = hub.client # type: Any
event, hint = event_from_exception(
(ty, value, tb),
client_options=client.options,
mechanism={"type": "tornado", "handled": False},
)
hub.capture_event(event, hint=hint)
def _make_event_processor(weak_handler):
# type: (Callable[[], RequestHandler]) -> Callable
def tornado_processor(event, hint):
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
handler = weak_handler()
if handler is None:
return event
request = handler.request
with capture_internal_exceptions():
method = getattr(handler, handler.request.method.lower())
event["transaction"] = transaction_from_function(method)
with capture_internal_exceptions():
extractor = TornadoRequestExtractor(request)
extractor.extract_into_event(event)
request_info = event["request"]
request_info["url"] = "%s://%s%s" % (
request.protocol,
request.host,
request.path,
)
request_info["query_string"] = request.query
request_info["method"] = request.method
request_info["env"] = {"REMOTE_ADDR": request.remote_ip}
request_info["headers"] = _filter_headers(dict(request.headers))
with capture_internal_exceptions():
if handler.current_user and _should_send_default_pii():
event.setdefault("user", {})["is_authenticated"] = True
return event
return tornado_processor
class TornadoRequestExtractor(RequestExtractor):
def content_length(self):
# type: () -> int
if self.request.body is None:
return 0
return len(self.request.body)
def cookies(self):
# type: () -> Dict
return {k: v.value for k, v in iteritems(self.request.cookies)}
def raw_data(self):
# type: () -> bytes
return self.request.body
def form(self):
# type: () -> Optional[Any]
return {
k: [v.decode("latin1", "replace") for v in vs]
for k, vs in iteritems(self.request.body_arguments)
}
def is_json(self):
# type: () -> bool
return _is_json_content_type(self.request.headers.get("content-type"))
def files(self):
# type: () -> Dict
return {k: v[0] for k, v in iteritems(self.request.files) if v}
def size_of_file(self, file):
return len(file.body or ())

View File

@ -0,0 +1,290 @@
import functools
import sys
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.utils import (
ContextVar,
capture_internal_exceptions,
event_from_exception,
)
from sentry_sdk._compat import PY2, reraise, iteritems
from sentry_sdk.tracing import Span
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk._types import MYPY
if MYPY:
from typing import Callable
from typing import Dict
from typing import List
from typing import Iterator
from typing import Any
from typing import Tuple
from typing import Optional
from typing import TypeVar
from sentry_sdk.utils import ExcInfo
T = TypeVar("T")
U = TypeVar("U")
E = TypeVar("E")
_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied")
if PY2:
def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
# type: (str, str, str) -> str
return s.decode(charset, errors)
else:
def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
# type: (str, str, str) -> str
return s.encode("latin1").decode(charset, errors)
def get_host(environ):
# type: (Dict[str, str]) -> str
"""Return the host for the given WSGI environment. Yanked from Werkzeug."""
if environ.get("HTTP_HOST"):
rv = environ["HTTP_HOST"]
if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"):
rv = rv[:-3]
elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"):
rv = rv[:-4]
elif environ.get("SERVER_NAME"):
rv = environ["SERVER_NAME"]
if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in (
("https", "443"),
("http", "80"),
):
rv += ":" + environ["SERVER_PORT"]
else:
# In spite of the WSGI spec, SERVER_NAME might not be present.
rv = "unknown"
return rv
def get_request_url(environ):
# type: (Dict[str, str]) -> str
"""Return the absolute URL without query string for the given WSGI
environment."""
return "%s://%s/%s" % (
environ.get("wsgi.url_scheme"),
get_host(environ),
wsgi_decoding_dance(environ.get("PATH_INFO") or "").lstrip("/"),
)
class SentryWsgiMiddleware(object):
__slots__ = ("app",)
def __init__(self, app):
# type: (Callable) -> None
self.app = app
def __call__(self, environ, start_response):
# type: (Dict[str, str], Callable) -> _ScopedResponse
if _wsgi_middleware_applied.get(False):
return self.app(environ, start_response)
_wsgi_middleware_applied.set(True)
try:
hub = Hub(Hub.current)
with hub:
with capture_internal_exceptions():
with hub.configure_scope() as scope:
scope.clear_breadcrumbs()
scope._name = "wsgi"
scope.add_event_processor(_make_wsgi_event_processor(environ))
span = Span.continue_from_environ(environ)
span.op = "http.server"
span.transaction = "generic WSGI request"
with hub.start_span(span) as span:
try:
rv = self.app(
environ,
functools.partial(
_sentry_start_response, start_response, span
),
)
except BaseException:
reraise(*_capture_exception(hub))
finally:
_wsgi_middleware_applied.set(False)
return _ScopedResponse(hub, rv)
def _sentry_start_response(
old_start_response, span, status, response_headers, exc_info=None
):
# type: (Callable[[str, U, Optional[E]], T], Span, str, U, Optional[E]) -> T
with capture_internal_exceptions():
status_int = int(status.split(" ", 1)[0])
span.set_tag("http.status_code", status_int)
if 500 <= status_int < 600:
span.set_failure()
return old_start_response(status, response_headers, exc_info)
def _get_environ(environ):
# type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
"""
Returns our whitelisted environment variables.
"""
keys = ["SERVER_NAME", "SERVER_PORT"]
if _should_send_default_pii():
# make debugging of proxy setup easier. Proxy headers are
# in headers.
keys += ["REMOTE_ADDR"]
for key in keys:
if key in environ:
yield key, environ[key]
# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders`
#
# We need this function because Django does not give us a "pure" http header
# dict. So we might as well use it for all WSGI integrations.
def _get_headers(environ):
# type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
"""
Returns only proper HTTP headers.
"""
for key, value in iteritems(environ):
key = str(key)
if key.startswith("HTTP_") and key not in (
"HTTP_CONTENT_TYPE",
"HTTP_CONTENT_LENGTH",
):
yield key[5:].replace("_", "-").title(), value
elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
yield key.replace("_", "-").title(), value
def get_client_ip(environ):
# type: (Dict[str, str]) -> Optional[Any]
"""
Infer the user IP address from various headers. This cannot be used in
security sensitive situations since the value may be forged from a client,
but it's good enough for the event payload.
"""
try:
return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip()
except (KeyError, IndexError):
pass
try:
return environ["HTTP_X_REAL_IP"]
except KeyError:
pass
return environ.get("REMOTE_ADDR")
def _capture_exception(hub):
# type: (Hub) -> ExcInfo
exc_info = sys.exc_info()
# Check client here as it might have been unset while streaming response
if hub.client is not None:
e = exc_info[1]
# SystemExit(0) is the only uncaught exception that is expected behavior
should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None)
if not should_skip_capture:
event, hint = event_from_exception(
exc_info,
client_options=hub.client.options,
mechanism={"type": "wsgi", "handled": False},
)
hub.capture_event(event, hint=hint)
return exc_info
class _ScopedResponse(object):
__slots__ = ("_response", "_hub")
def __init__(self, hub, response):
# type: (Hub, List[bytes]) -> None
self._hub = hub
self._response = response
def __iter__(self):
# type: () -> Iterator[bytes]
iterator = iter(self._response)
while True:
with self._hub:
try:
chunk = next(iterator)
except StopIteration:
break
except BaseException:
reraise(*_capture_exception(self._hub))
yield chunk
def close(self):
with self._hub:
try:
self._response.close()
except AttributeError:
pass
except BaseException:
reraise(*_capture_exception(self._hub))
def _make_wsgi_event_processor(environ):
# type: (Dict[str, str]) -> Callable
# It's a bit unfortunate that we have to extract and parse the request data
# from the environ so eagerly, but there are a few good reasons for this.
#
# We might be in a situation where the scope/hub never gets torn down
# properly. In that case we will have an unnecessary strong reference to
# all objects in the environ (some of which may take a lot of memory) when
# we're really just interested in a few of them.
#
# Keeping the environment around for longer than the request lifecycle is
# also not necessarily something uWSGI can deal with:
# https://github.com/unbit/uwsgi/issues/1950
client_ip = get_client_ip(environ)
request_url = get_request_url(environ)
query_string = environ.get("QUERY_STRING")
method = environ.get("REQUEST_METHOD")
env = dict(_get_environ(environ))
headers = _filter_headers(dict(_get_headers(environ)))
def event_processor(event, hint):
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
with capture_internal_exceptions():
# if the code below fails halfway through we at least have some data
request_info = event.setdefault("request", {})
if _should_send_default_pii():
user_info = event.setdefault("user", {})
user_info["ip_address"] = client_ip
request_info["url"] = request_url
request_info["query_string"] = query_string
request_info["method"] = method
request_info["env"] = env
request_info["headers"] = headers
return event
return event_processor

0
sentry_sdk/py.typed Normal file
View File

324
sentry_sdk/scope.py Normal file
View File

@ -0,0 +1,324 @@
from copy import copy
from collections import deque
from functools import wraps
from itertools import chain
from sentry_sdk.utils import logger, capture_internal_exceptions
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
from typing import Dict
from typing import Optional
from typing import Deque
from typing import List
from typing import Callable
from typing import TypeVar
from sentry_sdk._types import (
Breadcrumb,
Event,
EventProcessor,
ErrorProcessor,
Hint,
)
F = TypeVar("F", bound=Callable[..., Any])
global_event_processors = [] # type: List[EventProcessor]
def add_global_event_processor(processor):
# type: (EventProcessor) -> None
global_event_processors.append(processor)
def _attr_setter(fn):
return property(fset=fn, doc=fn.__doc__)
def _disable_capture(fn):
# type: (F) -> F
@wraps(fn)
def wrapper(self, *args, **kwargs):
# type: (Any, *Dict[str, Any], **Any) -> Any
if not self._should_capture:
return
try:
self._should_capture = False
return fn(self, *args, **kwargs)
finally:
self._should_capture = True
return wrapper # type: ignore
class Scope(object):
"""The scope holds extra information that should be sent with all
events that belong to it.
"""
__slots__ = (
"_level",
"_name",
"_fingerprint",
"_transaction",
"_user",
"_tags",
"_contexts",
"_extras",
"_breadcrumbs",
"_event_processors",
"_error_processors",
"_should_capture",
"_span",
)
def __init__(self):
# type: () -> None
self._event_processors = [] # type: List[EventProcessor]
self._error_processors = [] # type: List[ErrorProcessor]
self._name = None # type: Optional[str]
self.clear()
@_attr_setter
def level(self, value):
"""When set this overrides the level."""
self._level = value
@_attr_setter
def fingerprint(self, value):
"""When set this overrides the default fingerprint."""
self._fingerprint = value
@_attr_setter
def transaction(self, value):
"""When set this forces a specific transaction name to be set."""
self._transaction = value
if self._span:
self._span.transaction = value
@_attr_setter
def user(self, value):
"""When set a specific user is bound to the scope."""
self._user = value
@property
def span(self):
"""Get/set current tracing span."""
return self._span
@span.setter
def span(self, span):
self._span = span
if span is not None and span.transaction:
self._transaction = span.transaction
def set_tag(
self,
key, # type: str
value, # type: Any
):
# type: (...) -> None
"""Sets a tag for a key to a specific value."""
self._tags[key] = value
def remove_tag(
self, key # type: str
):
# type: (...) -> None
"""Removes a specific tag."""
self._tags.pop(key, None)
def set_context(
self,
key, # type: str
value, # type: Any
):
# type: (...) -> None
"""Binds a context at a certain key to a specific value."""
self._contexts[key] = value
def remove_context(
self, key # type: str
):
# type: (...) -> None
"""Removes a context."""
self._contexts.pop(key, None)
def set_extra(
self,
key, # type: str
value, # type: Any
):
# type: (...) -> None
"""Sets an extra key to a specific value."""
self._extras[key] = value
def remove_extra(
self, key # type: str
):
# type: (...) -> None
"""Removes a specific extra key."""
self._extras.pop(key, None)
def clear(self):
# type: () -> None
"""Clears the entire scope."""
self._level = None
self._fingerprint = None
self._transaction = None
self._user = None
self._tags = {} # type: Dict[str, Any]
self._contexts = {} # type: Dict[str, Dict[str, Any]]
self._extras = {} # type: Dict[str, Any]
self.clear_breadcrumbs()
self._should_capture = True
self._span = None
def clear_breadcrumbs(self):
# type: () -> None
"""Clears breadcrumb buffer."""
self._breadcrumbs = deque() # type: Deque[Breadcrumb]
def add_event_processor(
self, func # type: EventProcessor
):
# type: (...) -> None
"""Register a scope local event processor on the scope.
:param func: This function behaves like `before_send.`
"""
if len(self._event_processors) > 20:
logger.warning(
"Too many event processors on scope! Clearing list to free up some memory: %r",
self._event_processors,
)
del self._event_processors[:]
self._event_processors.append(func)
def add_error_processor(
self,
func, # type: ErrorProcessor
cls=None, # type: Optional[type]
):
# type: (...) -> None
"""Register a scope local error processor on the scope.
:param func: A callback that works similar to an event processor but is invoked with the original exception info triple as second argument.
:param cls: Optionally, only process exceptions of this type.
"""
if cls is not None:
cls_ = cls # For mypy.
real_func = func
def func(event, exc_info):
try:
is_inst = isinstance(exc_info[1], cls_)
except Exception:
is_inst = False
if is_inst:
return real_func(event, exc_info)
return event
self._error_processors.append(func)
@_disable_capture
def apply_to_event(
self,
event, # type: Event
hint, # type: Hint
):
# type: (...) -> Optional[Event]
"""Applies the information contained on the scope to the given event."""
def _drop(event, cause, ty):
# type: (Dict[str, Any], Any, str) -> Optional[Any]
logger.info("%s (%s) dropped event (%s)", ty, cause, event)
return None
if self._level is not None:
event["level"] = self._level
if event.get("type") != "transaction":
event.setdefault("breadcrumbs", []).extend(self._breadcrumbs)
if event.get("user") is None and self._user is not None:
event["user"] = self._user
if event.get("transaction") is None and self._transaction is not None:
event["transaction"] = self._transaction
if event.get("fingerprint") is None and self._fingerprint is not None:
event["fingerprint"] = self._fingerprint
if self._extras:
event.setdefault("extra", {}).update(self._extras)
if self._tags:
event.setdefault("tags", {}).update(self._tags)
if self._contexts:
event.setdefault("contexts", {}).update(self._contexts)
if self._span is not None:
contexts = event.setdefault("contexts", {})
if not contexts.get("trace"):
contexts["trace"] = self._span.get_trace_context()
exc_info = hint.get("exc_info")
if exc_info is not None:
for error_processor in self._error_processors:
new_event = error_processor(event, exc_info)
if new_event is None:
return _drop(event, error_processor, "error processor")
event = new_event
for event_processor in chain(global_event_processors, self._event_processors):
new_event = event
with capture_internal_exceptions():
new_event = event_processor(event, hint)
if new_event is None:
return _drop(event, event_processor, "event processor")
event = new_event
return event
def __copy__(self):
# type: () -> Scope
rv = object.__new__(self.__class__) # type: Scope
rv._level = self._level
rv._name = self._name
rv._fingerprint = self._fingerprint
rv._transaction = self._transaction
rv._user = self._user
rv._tags = dict(self._tags)
rv._contexts = dict(self._contexts)
rv._extras = dict(self._extras)
rv._breadcrumbs = copy(self._breadcrumbs)
rv._event_processors = list(self._event_processors)
rv._error_processors = list(self._error_processors)
rv._should_capture = self._should_capture
rv._span = self._span
return rv
def __repr__(self):
# type: () -> str
return "<%s id=%s name=%s>" % (
self.__class__.__name__,
hex(id(self)),
self._name,
)

297
sentry_sdk/serializer.py Normal file
View File

@ -0,0 +1,297 @@
import contextlib
from datetime import datetime
from sentry_sdk.utils import (
AnnotatedValue,
capture_internal_exceptions,
safe_repr,
strip_string,
)
from sentry_sdk._compat import text_type, PY2, string_types, number_types, iteritems
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Callable
from typing import Union
from typing import Generator
# https://github.com/python/mypy/issues/5710
_NotImplemented = Any
ReprProcessor = Callable[[Any, Dict[str, Any]], Union[_NotImplemented, str]]
Segment = Union[str, int]
if PY2:
# Importing ABCs from collections is deprecated, and will stop working in 3.8
# https://github.com/python/cpython/blob/master/Lib/collections/__init__.py#L49
from collections import Mapping, Sequence
else:
# New in 3.3
# https://docs.python.org/3/library/collections.abc.html
from collections.abc import Mapping, Sequence
MAX_DATABAG_DEPTH = 5
MAX_DATABAG_BREADTH = 10
CYCLE_MARKER = u"<cyclic>"
global_repr_processors = [] # type: List[ReprProcessor]
def add_global_repr_processor(processor):
# type: (ReprProcessor) -> None
global_repr_processors.append(processor)
class MetaNode(object):
__slots__ = (
"_parent",
"_segment",
"_depth",
"_data",
"_is_databag",
"_should_repr_strings",
)
def __init__(self):
# type: () -> None
self._parent = None # type: Optional[MetaNode]
self._segment = None # type: Optional[Segment]
self._depth = 0 # type: int
self._data = None # type: Optional[Dict[str, Any]]
self._is_databag = None # type: Optional[bool]
self._should_repr_strings = None # type: Optional[bool]
def startswith_path(self, path):
# type: (List[Optional[str]]) -> bool
if len(path) > self._depth:
return False
return self.is_path(path + [None] * (self._depth - len(path)))
def is_path(self, path):
# type: (List[Optional[str]]) -> bool
if len(path) != self._depth:
return False
cur = self
for segment in reversed(path):
if segment is not None and segment != cur._segment:
return False
assert cur._parent is not None
cur = cur._parent
return cur._segment is None
def enter(self, segment):
# type: (Segment) -> MetaNode
rv = MetaNode()
rv._parent = self
rv._depth = self._depth + 1
rv._segment = segment
return rv
def _create_annotations(self):
# type: () -> None
if self._data is not None:
return
self._data = {}
if self._parent is not None:
self._parent._create_annotations()
self._parent._data[str(self._segment)] = self._data # type: ignore
def annotate(self, **meta):
# type: (Any) -> None
self._create_annotations()
assert self._data is not None
self._data.setdefault("", {}).update(meta)
def should_repr_strings(self):
# type: () -> bool
if self._should_repr_strings is None:
self._should_repr_strings = (
self.startswith_path(
["exception", "values", None, "stacktrace", "frames", None, "vars"]
)
or self.startswith_path(
["threads", "values", None, "stacktrace", "frames", None, "vars"]
)
or self.startswith_path(["stacktrace", "frames", None, "vars"])
)
return self._should_repr_strings
def is_databag(self):
# type: () -> bool
if self._is_databag is None:
self._is_databag = (
self.startswith_path(["request", "data"])
or self.startswith_path(["breadcrumbs", None])
or self.startswith_path(["extra"])
or self.startswith_path(
["exception", "values", None, "stacktrace", "frames", None, "vars"]
)
or self.startswith_path(
["threads", "values", None, "stacktrace", "frames", None, "vars"]
)
or self.startswith_path(["stacktrace", "frames", None, "vars"])
)
return self._is_databag
def _flatten_annotated(obj, meta_node):
# type: (Any, MetaNode) -> Any
if isinstance(obj, AnnotatedValue):
meta_node.annotate(**obj.metadata)
obj = obj.value
return obj
class Memo(object):
def __init__(self):
# type: () -> None
self._inner = {} # type: Dict[int, Any]
@contextlib.contextmanager
def memoize(self, obj):
# type: (Any) -> Generator[bool, None, None]
if id(obj) in self._inner:
yield True
else:
self._inner[id(obj)] = obj
yield False
self._inner.pop(id(obj), None)
class Serializer(object):
def __init__(self):
# type: () -> None
self.memo = Memo()
self.meta_node = MetaNode()
@contextlib.contextmanager
def enter(self, segment):
# type: (Segment) -> Generator[None, None, None]
old_node = self.meta_node
self.meta_node = self.meta_node.enter(segment)
try:
yield
finally:
self.meta_node = old_node
def serialize_event(self, obj):
# type: (Any) -> Dict[str, Any]
rv = self._serialize_node(obj)
if self.meta_node._data is not None:
rv["_meta"] = self.meta_node._data
return rv
def _serialize_node(self, obj, max_depth=None, max_breadth=None):
# type: (Any, Optional[int], Optional[int]) -> Any
with capture_internal_exceptions():
with self.memo.memoize(obj) as result:
if result:
return CYCLE_MARKER
return self._serialize_node_impl(
obj, max_depth=max_depth, max_breadth=max_breadth
)
if self.meta_node.is_databag():
return u"<failed to serialize, use init(debug=True) to see error logs>"
return None
def _serialize_node_impl(self, obj, max_depth, max_breadth):
# type: (Any, Optional[int], Optional[int]) -> Any
if max_depth is None and max_breadth is None and self.meta_node.is_databag():
max_depth = self.meta_node._depth + MAX_DATABAG_DEPTH
max_breadth = self.meta_node._depth + MAX_DATABAG_BREADTH
if max_depth is None:
remaining_depth = None
else:
remaining_depth = max_depth - self.meta_node._depth
obj = _flatten_annotated(obj, self.meta_node)
if remaining_depth is not None and remaining_depth <= 0:
self.meta_node.annotate(rem=[["!limit", "x"]])
if self.meta_node.is_databag():
return _flatten_annotated(strip_string(safe_repr(obj)), self.meta_node)
return None
if self.meta_node.is_databag():
hints = {"memo": self.memo, "remaining_depth": remaining_depth}
for processor in global_repr_processors:
with capture_internal_exceptions():
result = processor(obj, hints)
if result is not NotImplemented:
return _flatten_annotated(result, self.meta_node)
if isinstance(obj, Mapping):
# Create temporary list here to avoid calling too much code that
# might mutate our dictionary while we're still iterating over it.
items = []
for i, (k, v) in enumerate(iteritems(obj)):
if max_breadth is not None and i >= max_breadth:
self.meta_node.annotate(len=max_breadth)
break
items.append((k, v))
rv_dict = {} # type: Dict[Any, Any]
for k, v in items:
k = text_type(k)
with self.enter(k):
v = self._serialize_node(
v, max_depth=max_depth, max_breadth=max_breadth
)
if v is not None:
rv_dict[k] = v
return rv_dict
elif isinstance(obj, Sequence) and not isinstance(obj, string_types):
rv_list = [] # type: List[Any]
for i, v in enumerate(obj):
if max_breadth is not None and i >= max_breadth:
self.meta_node.annotate(len=max_breadth)
break
with self.enter(i):
rv_list.append(
self._serialize_node(
v, max_depth=max_depth, max_breadth=max_breadth
)
)
return rv_list
if self.meta_node.should_repr_strings():
obj = safe_repr(obj)
else:
if obj is None or isinstance(obj, (bool, number_types)):
return obj
if isinstance(obj, datetime):
return text_type(obj.strftime("%Y-%m-%dT%H:%M:%S.%fZ"))
if isinstance(obj, bytes):
obj = obj.decode("utf-8", "replace")
if not isinstance(obj, string_types):
obj = safe_repr(obj)
return _flatten_annotated(strip_string(obj), self.meta_node)

454
sentry_sdk/tracing.py Normal file
View File

@ -0,0 +1,454 @@
import re
import uuid
import contextlib
from datetime import datetime
import sentry_sdk
from sentry_sdk.utils import capture_internal_exceptions, logger
from sentry_sdk._compat import PY2
from sentry_sdk._types import MYPY
if PY2:
from collections import Mapping
else:
from collections.abc import Mapping
if MYPY:
import typing
from typing import Generator
from typing import Optional
from typing import Any
from typing import Dict
from typing import List
from typing import Tuple
_traceparent_header_format_re = re.compile(
"^[ \t]*" # whitespace
"([0-9a-f]{32})?" # trace_id
"-?([0-9a-f]{16})?" # span_id
"-?([01])?" # sampled
"[ \t]*$" # whitespace
)
class EnvironHeaders(Mapping): # type: ignore
def __init__(
self,
environ, # type: typing.Mapping[str, str]
prefix="HTTP_", # type: str
):
# type: (...) -> None
self.environ = environ
self.prefix = prefix
def __getitem__(self, key):
# type: (str) -> Optional[Any]
return self.environ[self.prefix + key.replace("-", "_").upper()]
def __len__(self):
# type: () -> int
return sum(1 for _ in iter(self))
def __iter__(self):
# type: () -> Generator[str, None, None]
for k in self.environ:
if not isinstance(k, str):
continue
k = k.replace("-", "_").upper()
if not k.startswith(self.prefix):
continue
yield k[len(self.prefix) :]
class _SpanRecorder(object):
__slots__ = ("maxlen", "finished_spans", "open_span_count")
def __init__(self, maxlen):
# type: (int) -> None
self.maxlen = maxlen
self.open_span_count = 0 # type: int
self.finished_spans = [] # type: List[Span]
def start_span(self, span):
# type: (Span) -> None
# This is just so that we don't run out of memory while recording a lot
# of spans. At some point we just stop and flush out the start of the
# trace tree (i.e. the first n spans with the smallest
# start_timestamp).
self.open_span_count += 1
if self.open_span_count > self.maxlen:
span._span_recorder = None
def finish_span(self, span):
# type: (Span) -> None
self.finished_spans.append(span)
class Span(object):
__slots__ = (
"trace_id",
"span_id",
"parent_span_id",
"same_process_as_parent",
"sampled",
"transaction",
"op",
"description",
"start_timestamp",
"timestamp",
"_tags",
"_data",
"_span_recorder",
"hub",
"_context_manager_state",
)
def __init__(
self,
trace_id=None, # type: Optional[str]
span_id=None, # type: Optional[str]
parent_span_id=None, # type: Optional[str]
same_process_as_parent=True, # type: bool
sampled=None, # type: Optional[bool]
transaction=None, # type: Optional[str]
op=None, # type: Optional[str]
description=None, # type: Optional[str]
hub=None, # type: Optional[sentry_sdk.Hub]
):
# type: (...) -> None
self.trace_id = trace_id or uuid.uuid4().hex
self.span_id = span_id or uuid.uuid4().hex[16:]
self.parent_span_id = parent_span_id
self.same_process_as_parent = same_process_as_parent
self.sampled = sampled
self.transaction = transaction
self.op = op
self.description = description
self.hub = hub
self._tags = {} # type: Dict[str, str]
self._data = {} # type: Dict[str, Any]
self.start_timestamp = datetime.now()
#: End timestamp of span
self.timestamp = None # type: Optional[datetime]
self._span_recorder = None # type: Optional[_SpanRecorder]
def init_finished_spans(self, maxlen):
# type: (int) -> None
if self._span_recorder is None:
self._span_recorder = _SpanRecorder(maxlen)
self._span_recorder.start_span(self)
def __repr__(self):
# type: () -> str
return (
"<%s(transaction=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r)>"
% (
self.__class__.__name__,
self.transaction,
self.trace_id,
self.span_id,
self.parent_span_id,
self.sampled,
)
)
def __enter__(self):
# type: () -> Span
hub = self.hub or sentry_sdk.Hub.current
_, scope = hub._stack[-1]
old_span = scope.span
scope.span = self
self._context_manager_state = (hub, scope, old_span)
return self
def __exit__(self, ty, value, tb):
# type: (Optional[Any], Optional[Any], Optional[Any]) -> None
if value is not None:
self.set_failure()
hub, scope, old_span = self._context_manager_state
del self._context_manager_state
self.finish(hub)
scope.span = old_span
def new_span(self, **kwargs):
# type: (**Any) -> Span
rv = type(self)(
trace_id=self.trace_id,
span_id=None,
parent_span_id=self.span_id,
sampled=self.sampled,
**kwargs
)
rv._span_recorder = self._span_recorder
return rv
@classmethod
def continue_from_environ(cls, environ):
# type: (typing.Mapping[str, str]) -> Span
return cls.continue_from_headers(EnvironHeaders(environ))
@classmethod
def continue_from_headers(cls, headers):
# type: (typing.Mapping[str, str]) -> Span
parent = cls.from_traceparent(headers.get("sentry-trace"))
if parent is None:
return cls()
return parent.new_span(same_process_as_parent=False)
def iter_headers(self):
# type: () -> Generator[Tuple[str, str], None, None]
yield "sentry-trace", self.to_traceparent()
@classmethod
def from_traceparent(cls, traceparent):
# type: (Optional[str]) -> Optional[Span]
if not traceparent:
return None
if traceparent.startswith("00-") and traceparent.endswith("-00"):
traceparent = traceparent[3:-3]
match = _traceparent_header_format_re.match(str(traceparent))
if match is None:
return None
trace_id, span_id, sampled_str = match.groups()
if trace_id is not None:
trace_id = "{:032x}".format(int(trace_id, 16))
if span_id is not None:
span_id = "{:016x}".format(int(span_id, 16))
if sampled_str:
sampled = sampled_str != "0" # type: Optional[bool]
else:
sampled = None
return cls(trace_id=trace_id, span_id=span_id, sampled=sampled)
def to_traceparent(self):
# type: () -> str
sampled = ""
if self.sampled is True:
sampled = "1"
if self.sampled is False:
sampled = "0"
return "%s-%s-%s" % (self.trace_id, self.span_id, sampled)
def to_legacy_traceparent(self):
# type: () -> str
return "00-%s-%s-00" % (self.trace_id, self.span_id)
def set_tag(self, key, value):
# type: (str, Any) -> None
self._tags[key] = value
def set_data(self, key, value):
# type: (str, Any) -> None
self._data[key] = value
def set_failure(self):
# type: () -> None
self.set_tag("status", "failure")
def set_success(self):
# type: () -> None
self.set_tag("status", "success")
def is_success(self):
# type: () -> bool
return self._tags.get("status") in (None, "success")
def finish(self, hub=None):
# type: (Optional[sentry_sdk.Hub]) -> Optional[str]
hub = hub or self.hub or sentry_sdk.Hub.current
if self.timestamp is not None:
# This transaction is already finished, so we should not flush it again.
return None
self.timestamp = datetime.now()
_maybe_create_breadcrumbs_from_span(hub, self)
if self._span_recorder is None:
return None
self._span_recorder.finish_span(self)
if self.transaction is None:
# If this has no transaction set we assume there's a parent
# transaction for this span that would be flushed out eventually.
return None
if hub.client is None:
# We have no client and therefore nowhere to send this transaction
# event.
return None
if not self.sampled:
# At this point a `sampled = None` should have already been
# resolved to a concrete decision. If `sampled` is `None`, it's
# likely that somebody used `with sentry_sdk.Hub.start_span(..)` on a
# non-transaction span and later decided to make it a transaction.
if self.sampled is None:
logger.warning("Discarding transaction Span without sampling decision")
return None
return hub.capture_event(
{
"type": "transaction",
"transaction": self.transaction,
"contexts": {"trace": self.get_trace_context()},
"timestamp": self.timestamp,
"start_timestamp": self.start_timestamp,
"spans": [
s.to_json()
for s in self._span_recorder.finished_spans
if s is not self
],
}
)
def to_json(self):
# type: () -> Any
rv = {
"trace_id": self.trace_id,
"span_id": self.span_id,
"parent_span_id": self.parent_span_id,
"same_process_as_parent": self.same_process_as_parent,
"transaction": self.transaction,
"op": self.op,
"description": self.description,
"start_timestamp": self.start_timestamp,
"timestamp": self.timestamp,
"tags": self._tags,
"data": self._data,
}
return rv
def get_trace_context(self):
# type: () -> Any
rv = {
"trace_id": self.trace_id,
"span_id": self.span_id,
"parent_span_id": self.parent_span_id,
"op": self.op,
"description": self.description,
}
if "status" in self._tags:
rv["status"] = self._tags["status"]
return rv
def _format_sql(cursor, sql):
# type: (Any, str) -> Optional[str]
real_sql = None
# If we're using psycopg2, it could be that we're
# looking at a query that uses Composed objects. Use psycopg2's mogrify
# function to format the query. We lose per-parameter trimming but gain
# accuracy in formatting.
try:
if hasattr(cursor, "mogrify"):
real_sql = cursor.mogrify(sql)
if isinstance(real_sql, bytes):
real_sql = real_sql.decode(cursor.connection.encoding)
except Exception:
real_sql = None
return real_sql or str(sql)
@contextlib.contextmanager
def record_sql_queries(
hub, # type: sentry_sdk.Hub
cursor, # type: Any
query, # type: Any
params_list, # type: Any
paramstyle, # type: Optional[str]
executemany, # type: bool
):
# type: (...) -> Generator[Span, None, None]
# TODO: Bring back capturing of params by default
if hub.client and hub.client.options["_experiments"].get(
"record_sql_params", False
):
if not params_list or params_list == [None]:
params_list = None
if paramstyle == "pyformat":
paramstyle = "format"
else:
params_list = None
paramstyle = None
query = _format_sql(cursor, query)
data = {"db.params": params_list, "db.paramstyle": paramstyle}
if executemany:
data["db.executemany"] = True
with capture_internal_exceptions():
hub.add_breadcrumb(message=query, category="query", data=data)
with hub.start_span(op="db", description=query) as span:
for k, v in data.items():
span.set_data(k, v)
yield span
@contextlib.contextmanager
def record_http_request(hub, url, method):
# type: (sentry_sdk.Hub, str, str) -> Generator[Dict[str, str], None, None]
data_dict = {"url": url, "method": method}
with hub.start_span(op="http", description="%s %s" % (url, method)) as span:
try:
yield data_dict
finally:
if span is not None:
if "status_code" in data_dict:
span.set_tag("http.status_code", data_dict["status_code"])
for k, v in data_dict.items():
span.set_data(k, v)
def _maybe_create_breadcrumbs_from_span(hub, span):
# type: (sentry_sdk.Hub, Span) -> None
if span.op == "redis":
hub.add_breadcrumb(
message=span.description, type="redis", category="redis", data=span._tags
)
elif span.op == "http" and span.is_success():
hub.add_breadcrumb(
type="http",
category="httplib",
data=span._data,
hint={"httplib_response": span._data.pop("httplib_response", None)},
)
elif span.op == "subprocess":
hub.add_breadcrumb(
type="subprocess",
category="subprocess",
message=span.description,
data=span._data,
hint={"popen_instance": span._data.pop("popen_instance", None)},
)

263
sentry_sdk/transport.py Normal file
View File

@ -0,0 +1,263 @@
from __future__ import print_function
import json
import io
import urllib3 # type: ignore
import certifi
import gzip
from datetime import datetime, timedelta
from sentry_sdk.utils import Dsn, logger, capture_internal_exceptions
from sentry_sdk.worker import BackgroundWorker
from sentry_sdk._types import MYPY
if MYPY:
from typing import Type
from typing import Any
from typing import Optional
from typing import Dict
from typing import Union
from typing import Callable
from urllib3.poolmanager import PoolManager # type: ignore
from urllib3.poolmanager import ProxyManager
from sentry_sdk._types import Event
try:
from urllib.request import getproxies
except ImportError:
from urllib import getproxies # type: ignore
class Transport(object):
"""Baseclass for all transports.
A transport is used to send an event to sentry.
"""
parsed_dsn = None # type: Optional[Dsn]
def __init__(
self, options=None # type: Optional[Dict[str, Any]]
):
# type: (...) -> None
self.options = options
if options and options["dsn"] is not None and options["dsn"]:
self.parsed_dsn = Dsn(options["dsn"])
else:
self.parsed_dsn = None
def capture_event(
self, event # type: Event
):
# type: (...) -> None
"""This gets invoked with the event dictionary when an event should
be sent to sentry.
"""
raise NotImplementedError()
def flush(
self,
timeout, # type: float
callback=None, # type: Optional[Any]
):
# type: (...) -> None
"""Wait `timeout` seconds for the current events to be sent out."""
pass
def kill(self):
# type: () -> None
"""Forcefully kills the transport."""
pass
def __del__(self):
# type: () -> None
try:
self.kill()
except Exception:
pass
class HttpTransport(Transport):
"""The default HTTP transport."""
def __init__(
self, options # type: Dict[str, Any]
):
# type: (...) -> None
from sentry_sdk.consts import VERSION
Transport.__init__(self, options)
assert self.parsed_dsn is not None
self._worker = BackgroundWorker()
self._auth = self.parsed_dsn.to_auth("sentry.python/%s" % VERSION)
self._disabled_until = None # type: Optional[datetime]
self._retry = urllib3.util.Retry()
self.options = options
self._pool = self._make_pool(
self.parsed_dsn,
http_proxy=options["http_proxy"],
https_proxy=options["https_proxy"],
ca_certs=options["ca_certs"],
)
from sentry_sdk import Hub
self.hub_cls = Hub
def _send_event(
self, event # type: Event
):
# type: (...) -> None
if self._disabled_until is not None:
if datetime.utcnow() < self._disabled_until:
return
self._disabled_until = None
body = io.BytesIO()
with gzip.GzipFile(fileobj=body, mode="w") as f:
f.write(json.dumps(event, allow_nan=False).encode("utf-8"))
assert self.parsed_dsn is not None
logger.debug(
"Sending event, type:%s level:%s event_id:%s project:%s host:%s"
% (
event.get("type") or "null",
event.get("level") or "null",
event.get("event_id") or "null",
self.parsed_dsn.project_id,
self.parsed_dsn.host,
)
)
response = self._pool.request(
"POST",
str(self._auth.store_api_url),
body=body.getvalue(),
headers={
"User-Agent": str(self._auth.client),
"X-Sentry-Auth": str(self._auth.to_header()),
"Content-Type": "application/json",
"Content-Encoding": "gzip",
},
)
try:
if response.status == 429:
self._disabled_until = datetime.utcnow() + timedelta(
seconds=self._retry.get_retry_after(response) or 60
)
return
elif response.status >= 300 or response.status < 200:
logger.error(
"Unexpected status code: %s (body: %s)",
response.status,
response.data,
)
finally:
response.close()
self._disabled_until = None
def _get_pool_options(self, ca_certs):
# type: (Optional[Any]) -> Dict[str, Any]
return {
"num_pools": 2,
"cert_reqs": "CERT_REQUIRED",
"ca_certs": ca_certs or certifi.where(),
}
def _make_pool(
self,
parsed_dsn, # type: Dsn
http_proxy, # type: Optional[str]
https_proxy, # type: Optional[str]
ca_certs, # type: Optional[Any]
):
# type: (...) -> Union[PoolManager, ProxyManager]
proxy = None
# try HTTPS first
if parsed_dsn.scheme == "https" and (https_proxy != ""):
proxy = https_proxy or getproxies().get("https")
# maybe fallback to HTTP proxy
if not proxy and (http_proxy != ""):
proxy = http_proxy or getproxies().get("http")
opts = self._get_pool_options(ca_certs)
if proxy:
return urllib3.ProxyManager(proxy, **opts)
else:
return urllib3.PoolManager(**opts)
def capture_event(
self, event # type: Event
):
# type: (...) -> None
hub = self.hub_cls.current
def send_event_wrapper():
# type: () -> None
with hub:
with capture_internal_exceptions():
self._send_event(event)
self._worker.submit(send_event_wrapper)
def flush(
self,
timeout, # type: float
callback=None, # type: Optional[Any]
):
# type: (...) -> None
logger.debug("Flushing HTTP transport")
if timeout > 0:
self._worker.flush(timeout, callback)
def kill(self):
# type: () -> None
logger.debug("Killing HTTP transport")
self._worker.kill()
class _FunctionTransport(Transport):
def __init__(
self, func # type: Callable[[Event], None]
):
# type: (...) -> None
Transport.__init__(self)
self._func = func
def capture_event(
self, event # type: Event
):
# type: (...) -> None
self._func(event)
return None
def make_transport(options):
# type: (Dict[str, Any]) -> Optional[Transport]
ref_transport = options["transport"]
# If no transport is given, we use the http transport class
if ref_transport is None:
transport_cls = HttpTransport # type: Type[Transport]
elif isinstance(ref_transport, Transport):
return ref_transport
elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport):
transport_cls = ref_transport
elif callable(ref_transport):
return _FunctionTransport(ref_transport) # type: ignore
# if a transport class is given only instanciate it if the dsn is not
# empty or None
if options["dsn"]:
return transport_cls(options)
return None

770
sentry_sdk/utils.py Normal file
View File

@ -0,0 +1,770 @@
import os
import sys
import linecache
import logging
from contextlib import contextmanager
from datetime import datetime
from sentry_sdk._compat import urlparse, text_type, implements_str, PY2
from sentry_sdk._types import MYPY
if MYPY:
from typing import Any
from typing import Callable
from typing import Dict
from typing import Iterator
from typing import List
from typing import Optional
from typing import Set
from typing import Tuple
from typing import Union
from types import FrameType
from types import TracebackType
import sentry_sdk
from sentry_sdk._types import ExcInfo
epoch = datetime(1970, 1, 1)
# The logger is created here but initialized in the debug support module
logger = logging.getLogger("sentry_sdk.errors")
MAX_STRING_LENGTH = 512
MAX_FORMAT_PARAM_LENGTH = 128
def _get_debug_hub():
# type: () -> Optional[sentry_sdk.Hub]
# This function is replaced by debug.py
pass
@contextmanager
def capture_internal_exceptions():
# type: () -> Iterator
try:
yield
except Exception:
hub = _get_debug_hub()
if hub is not None:
hub._capture_internal_exception(sys.exc_info())
def to_timestamp(value):
# type: (datetime) -> float
return (value - epoch).total_seconds()
def event_hint_with_exc_info(exc_info=None):
# type: (Optional[ExcInfo]) -> Dict[str, Optional[ExcInfo]]
"""Creates a hint with the exc info filled in."""
if exc_info is None:
exc_info = sys.exc_info()
else:
exc_info = exc_info_from_error(exc_info)
if exc_info[0] is None:
exc_info = None
return {"exc_info": exc_info}
class BadDsn(ValueError):
"""Raised on invalid DSNs."""
@implements_str
class Dsn(object):
"""Represents a DSN."""
def __init__(self, value):
# type: (Union[Dsn, str]) -> None
if isinstance(value, Dsn):
self.__dict__ = dict(value.__dict__)
return
parts = urlparse.urlsplit(text_type(value))
if parts.scheme not in (u"http", u"https"):
raise BadDsn("Unsupported scheme %r" % parts.scheme)
self.scheme = parts.scheme
self.host = parts.hostname
self.port = parts.port
if self.port is None:
self.port = self.scheme == "https" and 443 or 80
self.public_key = parts.username
if not self.public_key:
raise BadDsn("Missing public key")
self.secret_key = parts.password
path = parts.path.rsplit("/", 1)
try:
self.project_id = text_type(int(path.pop()))
except (ValueError, TypeError):
raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:])
self.path = "/".join(path) + "/"
@property
def netloc(self):
# type: () -> str
"""The netloc part of a DSN."""
rv = self.host
if (self.scheme, self.port) not in (("http", 80), ("https", 443)):
rv = "%s:%s" % (rv, self.port)
return rv
def to_auth(self, client=None):
# type: (Optional[Any]) -> Auth
"""Returns the auth info object for this dsn."""
return Auth(
scheme=self.scheme,
host=self.netloc,
path=self.path,
project_id=self.project_id,
public_key=self.public_key,
secret_key=self.secret_key,
client=client,
)
def __str__(self):
# type: () -> str
return "%s://%s%s@%s%s%s" % (
self.scheme,
self.public_key,
self.secret_key and "@" + self.secret_key or "",
self.netloc,
self.path,
self.project_id,
)
class Auth(object):
"""Helper object that represents the auth info."""
def __init__(
self,
scheme,
host,
project_id,
public_key,
secret_key=None,
version=7,
client=None,
path="/",
):
# type: (str, str, str, str, Optional[str], int, Optional[Any], str) -> None
self.scheme = scheme
self.host = host
self.path = path
self.project_id = project_id
self.public_key = public_key
self.secret_key = secret_key
self.version = version
self.client = client
@property
def store_api_url(self):
# type: () -> str
"""Returns the API url for storing events."""
return "%s://%s%sapi/%s/store/" % (
self.scheme,
self.host,
self.path,
self.project_id,
)
def to_header(self, timestamp=None):
# type: (Optional[datetime]) -> str
"""Returns the auth header a string."""
rv = [("sentry_key", self.public_key), ("sentry_version", self.version)]
if timestamp is not None:
rv.append(("sentry_timestamp", str(to_timestamp(timestamp))))
if self.client is not None:
rv.append(("sentry_client", self.client))
if self.secret_key is not None:
rv.append(("sentry_secret", self.secret_key))
return u"Sentry " + u", ".join("%s=%s" % (key, value) for key, value in rv)
class AnnotatedValue(object):
def __init__(self, value, metadata):
# type: (Optional[Any], Dict[str, Any]) -> None
self.value = value
self.metadata = metadata
if MYPY:
from typing import TypeVar
T = TypeVar("T")
Annotated = Union[AnnotatedValue, T]
def get_type_name(cls):
# type: (Optional[type]) -> Optional[str]
return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None)
def get_type_module(cls):
# type: (Optional[type]) -> Optional[str]
mod = getattr(cls, "__module__", None)
if mod not in (None, "builtins", "__builtins__"):
return mod
return None
def should_hide_frame(frame):
# type: (FrameType) -> bool
try:
mod = frame.f_globals["__name__"]
if mod.startswith("sentry_sdk."):
return True
except (AttributeError, KeyError):
pass
for flag_name in "__traceback_hide__", "__tracebackhide__":
try:
if frame.f_locals[flag_name]:
return True
except Exception:
pass
return False
def iter_stacks(tb):
# type: (Optional[TracebackType]) -> Iterator[TracebackType]
tb_ = tb # type: Optional[TracebackType]
while tb_ is not None:
if not should_hide_frame(tb_.tb_frame):
yield tb_
tb_ = tb_.tb_next
def get_lines_from_file(
filename, # type: str
lineno, # type: int
loader=None, # type: Optional[Any]
module=None, # type: Optional[str]
):
# type: (...) -> Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]
context_lines = 5
source = None
if loader is not None and hasattr(loader, "get_source"):
try:
source_str = loader.get_source(module) # type: Optional[str]
except (ImportError, IOError):
source_str = None
if source_str is not None:
source = source_str.splitlines()
if source is None:
try:
source = linecache.getlines(filename)
except (OSError, IOError):
return [], None, []
if not source:
return [], None, []
lower_bound = max(0, lineno - context_lines)
upper_bound = min(lineno + 1 + context_lines, len(source))
try:
pre_context = [
strip_string(line.strip("\r\n")) for line in source[lower_bound:lineno]
]
context_line = strip_string(source[lineno].strip("\r\n"))
post_context = [
strip_string(line.strip("\r\n"))
for line in source[(lineno + 1) : upper_bound]
]
return pre_context, context_line, post_context
except IndexError:
# the file may have changed since it was loaded into memory
return [], None, []
def get_source_context(
frame, # type: FrameType
tb_lineno, # type: int
):
# type: (...) -> Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]
try:
abs_path = frame.f_code.co_filename # type: Optional[str]
except Exception:
abs_path = None
try:
module = frame.f_globals["__name__"]
except Exception:
return [], None, []
try:
loader = frame.f_globals["__loader__"]
except Exception:
loader = None
lineno = tb_lineno - 1
if lineno is not None and abs_path:
return get_lines_from_file(abs_path, lineno, loader, module)
return [], None, []
def safe_str(value):
# type: (Any) -> str
try:
return text_type(value)
except Exception:
return safe_repr(value)
def safe_repr(value):
# type: (Any) -> str
try:
rv = repr(value)
if isinstance(rv, bytes):
rv = rv.decode("utf-8", "replace")
# At this point `rv` contains a bunch of literal escape codes, like
# this (exaggerated example):
#
# u"\\x2f"
#
# But we want to show this string as:
#
# u"/"
try:
# unicode-escape does this job, but can only decode latin1. So we
# attempt to encode in latin1.
return rv.encode("latin1").decode("unicode-escape")
except Exception:
# Since usually strings aren't latin1 this can break. In those
# cases we just give up.
return rv
except Exception:
# If e.g. the call to `repr` already fails
return u"<broken repr>"
def filename_for_module(module, abs_path):
# type: (Optional[str], Optional[str]) -> Optional[str]
if not abs_path or not module:
return abs_path
try:
if abs_path.endswith(".pyc"):
abs_path = abs_path[:-1]
base_module = module.split(".", 1)[0]
if base_module == module:
return os.path.basename(abs_path)
base_module_path = sys.modules[base_module].__file__
return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip(
os.sep
)
except Exception:
return abs_path
def serialize_frame(frame, tb_lineno=None, with_locals=True):
# type: (FrameType, Optional[int], bool) -> Dict[str, Any]
f_code = getattr(frame, "f_code", None)
if not f_code:
abs_path = None
function = None
else:
abs_path = frame.f_code.co_filename
function = frame.f_code.co_name
try:
module = frame.f_globals["__name__"]
except Exception:
module = None
if tb_lineno is None:
tb_lineno = frame.f_lineno
pre_context, context_line, post_context = get_source_context(frame, tb_lineno)
rv = {
"filename": filename_for_module(module, abs_path) or None,
"abs_path": os.path.abspath(abs_path) if abs_path else None,
"function": function or "<unknown>",
"module": module,
"lineno": tb_lineno,
"pre_context": pre_context,
"context_line": context_line,
"post_context": post_context,
} # type: Dict[str, Any]
if with_locals:
rv["vars"] = frame.f_locals
return rv
def stacktrace_from_traceback(tb=None, with_locals=True):
# type: (Optional[TracebackType], bool) -> Dict[str, List[Dict[str, Any]]]
return {
"frames": [
serialize_frame(
tb.tb_frame, tb_lineno=tb.tb_lineno, with_locals=with_locals
)
for tb in iter_stacks(tb)
]
}
def current_stacktrace(with_locals=True):
# type: (bool) -> Any
__tracebackhide__ = True
frames = []
f = sys._getframe()
while f is not None:
if not should_hide_frame(f):
frames.append(serialize_frame(f, with_locals=with_locals))
f = f.f_back
frames.reverse()
return {"frames": frames}
def get_errno(exc_value):
# type: (BaseException) -> Optional[Any]
return getattr(exc_value, "errno", None)
def single_exception_from_error_tuple(
exc_type, # type: Optional[type]
exc_value, # type: Optional[BaseException]
tb, # type: Optional[TracebackType]
client_options=None, # type: Optional[dict]
mechanism=None, # type: Optional[Dict[str, Any]]
):
# type: (...) -> Dict[str, Any]
if exc_value is not None:
errno = get_errno(exc_value)
else:
errno = None
if errno is not None:
mechanism = mechanism or {}
mechanism.setdefault("meta", {}).setdefault("errno", {}).setdefault(
"number", errno
)
if client_options is None:
with_locals = True
else:
with_locals = client_options["with_locals"]
return {
"module": get_type_module(exc_type),
"type": get_type_name(exc_type),
"value": safe_str(exc_value),
"mechanism": mechanism,
"stacktrace": stacktrace_from_traceback(tb, with_locals),
}
HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__")
if HAS_CHAINED_EXCEPTIONS:
def walk_exception_chain(exc_info):
# type: (ExcInfo) -> Iterator[ExcInfo]
exc_type, exc_value, tb = exc_info
seen_exceptions = []
seen_exception_ids = set() # type: Set[int]
while (
exc_type is not None
and exc_value is not None
and id(exc_value) not in seen_exception_ids
):
yield exc_type, exc_value, tb
# Avoid hashing random types we don't know anything
# about. Use the list to keep a ref so that the `id` is
# not used for another object.
seen_exceptions.append(exc_value)
seen_exception_ids.add(id(exc_value))
if exc_value.__suppress_context__:
cause = exc_value.__cause__
else:
cause = exc_value.__context__
if cause is None:
break
exc_type = type(cause)
exc_value = cause
tb = getattr(cause, "__traceback__", None)
else:
def walk_exception_chain(exc_info):
# type: (ExcInfo) -> Iterator[ExcInfo]
yield exc_info
def exceptions_from_error_tuple(
exc_info, # type: ExcInfo
client_options=None, # type: Optional[dict]
mechanism=None, # type: Optional[Dict[str, Any]]
):
# type: (...) -> List[Dict[str, Any]]
exc_type, exc_value, tb = exc_info
rv = []
for exc_type, exc_value, tb in walk_exception_chain(exc_info):
rv.append(
single_exception_from_error_tuple(
exc_type, exc_value, tb, client_options, mechanism
)
)
rv.reverse()
return rv
def to_string(value):
# type: (str) -> str
try:
return text_type(value)
except UnicodeDecodeError:
return repr(value)[1:-1]
def iter_event_stacktraces(event):
# type: (Dict[str, Any]) -> Iterator[Dict[str, Any]]
if "stacktrace" in event:
yield event["stacktrace"]
if "threads" in event:
for thread in event["threads"].get("values") or ():
if "stacktrace" in thread:
yield thread["stacktrace"]
if "exception" in event:
for exception in event["exception"].get("values") or ():
if "stacktrace" in exception:
yield exception["stacktrace"]
def iter_event_frames(event):
# type: (Dict[str, Any]) -> Iterator[Dict[str, Any]]
for stacktrace in iter_event_stacktraces(event):
for frame in stacktrace.get("frames") or ():
yield frame
def handle_in_app(event, in_app_exclude=None, in_app_include=None):
# type: (Dict[str, Any], Optional[List], Optional[List]) -> Dict[str, Any]
for stacktrace in iter_event_stacktraces(event):
handle_in_app_impl(
stacktrace.get("frames"),
in_app_exclude=in_app_exclude,
in_app_include=in_app_include,
)
return event
def handle_in_app_impl(frames, in_app_exclude, in_app_include):
# type: (Any, Optional[List], Optional[List]) -> Optional[Any]
if not frames:
return None
any_in_app = False
for frame in frames:
in_app = frame.get("in_app")
if in_app is not None:
if in_app:
any_in_app = True
continue
module = frame.get("module")
if not module:
continue
elif _module_in_set(module, in_app_include):
frame["in_app"] = True
any_in_app = True
elif _module_in_set(module, in_app_exclude):
frame["in_app"] = False
if not any_in_app:
for frame in frames:
if frame.get("in_app") is None:
frame["in_app"] = True
return frames
def exc_info_from_error(error):
# type: (Union[BaseException, ExcInfo]) -> ExcInfo
if isinstance(error, tuple) and len(error) == 3:
exc_type, exc_value, tb = error
elif isinstance(error, BaseException):
tb = getattr(error, "__traceback__", None)
if tb is not None:
exc_type = type(error)
exc_value = error
else:
exc_type, exc_value, tb = sys.exc_info()
if exc_value is not error:
tb = None
exc_value = error
exc_type = type(error)
else:
raise ValueError()
return exc_type, exc_value, tb
def event_from_exception(
exc_info, # type: Union[BaseException, ExcInfo]
client_options=None, # type: Optional[dict]
mechanism=None, # type: Optional[Dict[str, Any]]
):
# type: (...) -> Tuple[Dict[str, Any], Dict[str, Any]]
exc_info = exc_info_from_error(exc_info)
hint = event_hint_with_exc_info(exc_info)
return (
{
"level": "error",
"exception": {
"values": exceptions_from_error_tuple(
exc_info, client_options, mechanism
)
},
},
hint,
)
def _module_in_set(name, set):
# type: (str, Optional[List]) -> bool
if not set:
return False
for item in set or ():
if item == name or name.startswith(item + "."):
return True
return False
def strip_string(value, max_length=None):
# type: (str, Optional[int]) -> Union[AnnotatedValue, str]
# TODO: read max_length from config
if not value:
return value
if max_length is None:
# This is intentionally not just the default such that one can patch `MAX_STRING_LENGTH` and affect `strip_string`.
max_length = MAX_STRING_LENGTH
length = len(value)
if length > max_length:
return AnnotatedValue(
value=value[: max_length - 3] + u"...",
metadata={
"len": length,
"rem": [["!limit", "x", max_length - 3, max_length]],
},
)
return value
def _is_threading_local_monkey_patched():
# type: () -> bool
try:
from gevent.monkey import is_object_patched # type: ignore
if is_object_patched("threading", "local"):
return True
except ImportError:
pass
try:
from eventlet.patcher import is_monkey_patched # type: ignore
if is_monkey_patched("thread"):
return True
except ImportError:
pass
return False
def _get_contextvars():
# () -> (bool, Type)
"""
Try to import contextvars and use it if it's deemed safe. We should not use
contextvars if gevent or eventlet have patched thread locals, as
contextvars are unaffected by that patch.
https://github.com/gevent/gevent/issues/1407
"""
if not _is_threading_local_monkey_patched():
try:
from contextvars import ContextVar # type: ignore
if not PY2 and sys.version_info < (3, 7):
import aiocontextvars # type: ignore # noqa
return True, ContextVar
except ImportError:
pass
from threading import local
class ContextVar(object): # type: ignore
# Super-limited impl of ContextVar
def __init__(self, name):
self._name = name
self._local = local()
def get(self, default):
return getattr(self._local, "value", default)
def set(self, value):
setattr(self._local, "value", value)
return False, ContextVar
HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
def transaction_from_function(func):
# type: (Callable[..., Any]) -> Optional[str]
# Methods in Python 2
try:
return "%s.%s.%s" % (
func.im_class.__module__, # type: ignore
func.im_class.__name__, # type: ignore
func.__name__,
)
except Exception:
pass
func_qualname = (
getattr(func, "__qualname__", None) or getattr(func, "__name__", None) or None
) # type: Optional[str]
if not func_qualname:
# No idea what it is
return None
# Methods in Python 3
# Functions
# Classes
try:
return "%s.%s" % (func.__module__, func_qualname)
except Exception:
pass
# Possibly a lambda
return func_qualname

132
sentry_sdk/worker.py Normal file
View File

@ -0,0 +1,132 @@
import os
from threading import Thread, Lock
from time import sleep, time
from sentry_sdk._compat import queue, check_thread_support
from sentry_sdk.utils import logger
from sentry_sdk._types import MYPY
if MYPY:
from queue import Queue
from typing import Any
from typing import Optional
from typing import Callable
_TERMINATOR = object()
class BackgroundWorker(object):
def __init__(self):
# type: () -> None
check_thread_support()
self._queue = queue.Queue(-1) # type: Queue[Any]
self._lock = Lock()
self._thread = None # type: Optional[Thread]
self._thread_for_pid = None # type: Optional[int]
@property
def is_alive(self):
# type: () -> bool
if self._thread_for_pid != os.getpid():
return False
if not self._thread:
return False
return self._thread.is_alive()
def _ensure_thread(self):
# type: () -> None
if not self.is_alive:
self.start()
def _timed_queue_join(self, timeout):
# type: (float) -> bool
deadline = time() + timeout
queue = self._queue
real_all_tasks_done = getattr(
queue, "all_tasks_done", None
) # type: Optional[Any]
if real_all_tasks_done is not None:
real_all_tasks_done.acquire()
all_tasks_done = real_all_tasks_done # type: Optional[Any]
elif queue.__module__.startswith("eventlet."):
all_tasks_done = getattr(queue, "_cond", None)
else:
all_tasks_done = None
try:
while queue.unfinished_tasks: # type: ignore
delay = deadline - time()
if delay <= 0:
return False
if all_tasks_done is not None:
all_tasks_done.wait(timeout=delay)
else:
# worst case, we just poll the number of remaining tasks
sleep(0.1)
return True
finally:
if real_all_tasks_done is not None:
real_all_tasks_done.release() # type: ignore
def start(self):
# type: () -> None
with self._lock:
if not self.is_alive:
self._thread = Thread(
target=self._target, name="raven-sentry.BackgroundWorker"
)
self._thread.setDaemon(True)
self._thread.start()
self._thread_for_pid = os.getpid()
def kill(self):
# type: () -> None
logger.debug("background worker got kill request")
with self._lock:
if self._thread:
self._queue.put_nowait(_TERMINATOR)
self._thread = None
self._thread_for_pid = None
def flush(self, timeout, callback=None):
# type: (float, Optional[Any]) -> None
logger.debug("background worker got flush request")
with self._lock:
if self.is_alive and timeout > 0.0:
self._wait_flush(timeout, callback)
logger.debug("background worker flushed")
def _wait_flush(self, timeout, callback):
# type: (float, Optional[Any]) -> None
initial_timeout = min(0.1, timeout)
if not self._timed_queue_join(initial_timeout):
pending = self._queue.qsize()
logger.debug("%d event(s) pending on flush", pending)
if callback is not None:
callback(pending, timeout)
self._timed_queue_join(timeout - initial_timeout)
def submit(self, callback):
# type: (Callable[[], None]) -> None
self._ensure_thread()
self._queue.put_nowait(callback)
def _target(self):
# type: () -> None
while True:
callback = self._queue.get()
try:
if callback is _TERMINATOR:
break
try:
callback()
except Exception:
logger.error("Failed processing job", exc_info=True)
finally:
self._queue.task_done()
sleep(0)

2
setup.cfg Normal file
View File

@ -0,0 +1,2 @@
[bdist_wheel]
universal = 1

48
setup.py Normal file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env python
"""
Sentry-Python - Sentry SDK for Python
=====================================
**Sentry-Python is an SDK for Sentry.** Check out `GitHub
<https://github.com/getsentry/sentry-python>`_ to find out more.
"""
from setuptools import setup, find_packages
setup(
name="sentry-sdk",
version="0.12.2",
author="Sentry Team and Contributors",
author_email="hello@getsentry.com",
url="https://github.com/getsentry/sentry-python",
description="Python client for Sentry (https://getsentry.com)",
long_description=__doc__,
packages=find_packages(exclude=("tests", "tests.*")),
# PEP 561
package_data={"sentry_sdk": ["py.typed"]},
zip_safe=False,
license="BSD",
install_requires=["urllib3>=1.9", "certifi"],
extras_require={
"flask": ["flask>=0.8", "blinker>=1.1"],
"bottle": ["bottle>=0.12.13"],
"falcon": ["falcon>=1.4"],
},
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Topic :: Software Development :: Libraries :: Python Modules",
],
)

9
test-requirements.txt Normal file
View File

@ -0,0 +1,9 @@
hypothesis==3.69.9
pytest==3.7.3
pytest-xdist==1.23.0
tox==3.7.0
Werkzeug==0.15.3
pytest-localserver==0.4.1
pytest-cov==2.6.0
gevent
eventlet

13
tests/__init__.py Normal file
View File

@ -0,0 +1,13 @@
import sys
import pytest
# This is used in _capture_internal_warnings. We need to run this at import
# time because that's where many deprecation warnings might get thrown.
#
# This lives in tests/__init__.py because apparently even tests/conftest.py
# gets loaded too late.
assert "sentry_sdk" not in sys.modules
_warning_recorder_mgr = pytest.warns(None)
_warning_recorder = _warning_recorder_mgr.__enter__()

269
tests/conftest.py Normal file
View File

@ -0,0 +1,269 @@
import os
import subprocess
import json
import pytest
import gevent
import eventlet
import sentry_sdk
from sentry_sdk._compat import reraise, string_types, iteritems
from sentry_sdk.transport import Transport
from tests import _warning_recorder, _warning_recorder_mgr
SEMAPHORE = "./semaphore"
if not os.path.isfile(SEMAPHORE):
SEMAPHORE = None
try:
import pytest_benchmark
except ImportError:
@pytest.fixture
def benchmark():
return lambda x: x()
else:
del pytest_benchmark
@pytest.fixture(autouse=True)
def internal_exceptions(request, monkeypatch):
errors = []
if "tests_internal_exceptions" in request.keywords:
return
def _capture_internal_exception(self, exc_info):
errors.append(exc_info)
@request.addfinalizer
def _():
for e in errors:
reraise(*e)
monkeypatch.setattr(
sentry_sdk.Hub, "_capture_internal_exception", _capture_internal_exception
)
return errors
@pytest.fixture(autouse=True, scope="session")
def _capture_internal_warnings():
yield
_warning_recorder_mgr.__exit__(None, None, None)
recorder = _warning_recorder
for warning in recorder:
try:
if isinstance(warning.message, ResourceWarning):
continue
except NameError:
pass
# pytest-django
if "getfuncargvalue" in str(warning.message):
continue
# Happens when re-initializing the SDK
if "but it was only enabled on init()" in str(warning.message):
continue
# sanic's usage of aiohttp for test client
if "verify_ssl is deprecated, use ssl=False instead" in str(warning.message):
continue
if "getargspec" in str(warning.message) and warning.filename.endswith(
("pyramid/config/util.py", "pyramid/config/views.py")
):
continue
if "isAlive() is deprecated" in str(
warning.message
) and warning.filename.endswith("celery/utils/timer2.py"):
continue
if "collections.abc" in str(warning.message) and warning.filename.endswith(
("celery/canvas.py", "werkzeug/datastructures.py", "tornado/httputil.py")
):
continue
# Django 1.7 emits a (seemingly) false-positive warning for our test
# app and suggests to use a middleware that does not exist in later
# Django versions.
if "SessionAuthenticationMiddleware" in str(warning.message):
continue
if "Something has already installed a non-asyncio" in str(warning.message):
continue
if "dns.hash" in str(warning.message) or "dns/namedict" in warning.filename:
continue
raise AssertionError(warning)
@pytest.fixture
def monkeypatch_test_transport(monkeypatch, semaphore_normalize):
def check_event(event):
def check_string_keys(map):
for key, value in iteritems(map):
assert isinstance(key, string_types)
if isinstance(value, dict):
check_string_keys(value)
check_string_keys(event)
semaphore_normalize(event)
def inner(client):
monkeypatch.setattr(client, "transport", TestTransport(check_event))
return inner
def _no_errors_in_semaphore_response(obj):
"""Assert that semaphore didn't throw any errors when processing the
event."""
def inner(obj):
if not isinstance(obj, dict):
return
assert "err" not in obj
for value in obj.values():
inner(value)
try:
inner(obj.get("_meta"))
inner(obj.get(""))
except AssertionError:
raise AssertionError(obj)
@pytest.fixture
def semaphore_normalize(tmpdir):
def inner(event):
if not SEMAPHORE:
return
# Disable subprocess integration
with sentry_sdk.Hub(None):
# not dealing with the subprocess API right now
file = tmpdir.join("event")
file.write(json.dumps(dict(event)))
output = json.loads(
subprocess.check_output(
[SEMAPHORE, "process-event"], stdin=file.open()
).decode("utf-8")
)
_no_errors_in_semaphore_response(output)
output.pop("_meta", None)
return output
return inner
@pytest.fixture
def sentry_init(monkeypatch_test_transport):
def inner(*a, **kw):
hub = sentry_sdk.Hub.current
client = sentry_sdk.Client(*a, **kw)
hub.bind_client(client)
monkeypatch_test_transport(sentry_sdk.Hub.current.client)
return inner
class TestTransport(Transport):
def __init__(self, capture_event_callback):
Transport.__init__(self)
self.capture_event = capture_event_callback
self._queue = None
@pytest.fixture
def capture_events(monkeypatch):
def inner():
events = []
test_client = sentry_sdk.Hub.current.client
old_capture_event = test_client.transport.capture_event
def append(event):
events.append(event)
return old_capture_event(event)
monkeypatch.setattr(test_client.transport, "capture_event", append)
return events
return inner
@pytest.fixture
def capture_events_forksafe(monkeypatch):
def inner():
events_r, events_w = os.pipe()
events_r = os.fdopen(events_r, "rb", 0)
events_w = os.fdopen(events_w, "wb", 0)
test_client = sentry_sdk.Hub.current.client
old_capture_event = test_client.transport.capture_event
def append(event):
events_w.write(json.dumps(event).encode("utf-8"))
events_w.write(b"\n")
return old_capture_event(event)
def flush(timeout=None, callback=None):
events_w.write(b"flush\n")
monkeypatch.setattr(test_client.transport, "capture_event", append)
monkeypatch.setattr(test_client, "flush", flush)
return EventStreamReader(events_r)
return inner
class EventStreamReader(object):
def __init__(self, file):
self.file = file
def read_event(self):
return json.loads(self.file.readline().decode("utf-8"))
def read_flush(self):
assert self.file.readline() == b"flush\n"
# scope=session ensures that fixture is run earlier
@pytest.fixture(scope="session", params=[None, "eventlet", "gevent"])
def maybe_monkeypatched_threading(request):
if request.param == "eventlet":
try:
eventlet.monkey_patch()
except AttributeError as e:
if "'thread.RLock' object has no attribute" in str(e):
# https://bitbucket.org/pypy/pypy/issues/2962/gevent-cannot-patch-rlock-under-pypy-27-7
pytest.skip("https://github.com/eventlet/eventlet/issues/546")
else:
raise
elif request.param == "gevent":
try:
gevent.monkey.patch_all()
except Exception as e:
if "_RLock__owner" in str(e):
pytest.skip("https://github.com/gevent/gevent/issues/1380")
else:
raise
else:
assert request.param is None
return request.param

View File

View File

@ -0,0 +1,3 @@
import pytest
aiohttp = pytest.importorskip("aiohttp")

View File

@ -0,0 +1,103 @@
from aiohttp import web
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
async def test_basic(sentry_init, aiohttp_client, loop, capture_events):
sentry_init(integrations=[AioHttpIntegration()])
async def hello(request):
1 / 0
app = web.Application()
app.router.add_get("/", hello)
events = capture_events()
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 500
event, = events
assert (
event["transaction"]
== "tests.integrations.aiohttp.test_aiohttp.test_basic.<locals>.hello"
)
exception, = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
request = event["request"]
host = request["headers"]["Host"]
assert request["env"] == {"REMOTE_ADDR": "127.0.0.1"}
assert request["method"] == "GET"
assert request["query_string"] == ""
assert request["url"] == "http://{host}/".format(host=host)
assert request["headers"] == {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": host,
"User-Agent": request["headers"]["User-Agent"],
}
async def test_403_not_captured(sentry_init, aiohttp_client, loop, capture_events):
sentry_init(integrations=[AioHttpIntegration()])
async def hello(request):
raise web.HTTPForbidden()
app = web.Application()
app.router.add_get("/", hello)
events = capture_events()
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 403
assert not events
async def test_half_initialized(sentry_init, aiohttp_client, loop, capture_events):
sentry_init(integrations=[AioHttpIntegration()])
sentry_init()
async def hello(request):
return web.Response(text="hello")
app = web.Application()
app.router.add_get("/", hello)
events = capture_events()
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 200
assert events == []
async def test_tracing(sentry_init, aiohttp_client, loop, capture_events):
sentry_init(integrations=[AioHttpIntegration()], traces_sample_rate=1.0)
async def hello(request):
return web.Response(text="hello")
app = web.Application()
app.router.add_get("/", hello)
events = capture_events()
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 200
event, = events
assert event["type"] == "transaction"
assert (
event["transaction"]
== "tests.integrations.aiohttp.test_aiohttp.test_tracing.<locals>.hello"
)

View File

@ -0,0 +1,16 @@
import sys
from sentry_sdk import capture_message
from sentry_sdk.integrations.argv import ArgvIntegration
def test_basic(sentry_init, capture_events, monkeypatch):
sentry_init(integrations=[ArgvIntegration()])
argv = ["foo", "bar", "baz"]
monkeypatch.setattr(sys, "argv", argv)
events = capture_events()
capture_message("hi")
event, = events
assert event["extra"]["sys.argv"] == argv

View File

@ -0,0 +1,3 @@
import pytest
pytest.importorskip("starlette")

View File

@ -0,0 +1,121 @@
import sys
import pytest
from sentry_sdk import capture_message
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.testclient import TestClient
@pytest.fixture
def app():
app = Starlette()
@app.route("/sync-message")
def hi(request):
capture_message("hi", level="error")
return PlainTextResponse("ok")
@app.route("/async-message")
async def hi2(request):
capture_message("hi", level="error")
return PlainTextResponse("ok")
app.add_middleware(SentryAsgiMiddleware)
return app
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
def test_sync_request_data(sentry_init, app, capture_events):
sentry_init(send_default_pii=True)
events = capture_events()
client = TestClient(app)
response = client.get("/sync-message?foo=bar", headers={"Foo": u"ä"})
assert response.status_code == 200
event, = events
assert event["transaction"] == "tests.integrations.asgi.test_asgi.app.<locals>.hi"
assert event["request"]["env"] == {"REMOTE_ADDR": "testclient"}
assert set(event["request"]["headers"]) == {
"accept",
"accept-encoding",
"connection",
"host",
"user-agent",
"foo",
}
assert event["request"]["query_string"] == "foo=bar"
assert event["request"]["url"].endswith("/sync-message")
assert event["request"]["method"] == "GET"
# Assert that state is not leaked
events.clear()
capture_message("foo")
event, = events
assert "request" not in event
assert "transaction" not in event
def test_async_request_data(sentry_init, app, capture_events):
sentry_init(send_default_pii=True)
events = capture_events()
client = TestClient(app)
response = client.get("/async-message?foo=bar")
assert response.status_code == 200
event, = events
assert event["transaction"] == "tests.integrations.asgi.test_asgi.app.<locals>.hi2"
assert event["request"]["env"] == {"REMOTE_ADDR": "testclient"}
assert set(event["request"]["headers"]) == {
"accept",
"accept-encoding",
"connection",
"host",
"user-agent",
}
assert event["request"]["query_string"] == "foo=bar"
assert event["request"]["url"].endswith("/async-message")
assert event["request"]["method"] == "GET"
# Assert that state is not leaked
events.clear()
capture_message("foo")
event, = events
assert "request" not in event
assert "transaction" not in event
def test_errors(sentry_init, app, capture_events):
sentry_init(send_default_pii=True)
events = capture_events()
@app.route("/error")
def myerror(request):
raise ValueError("oh no")
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/error")
assert response.status_code == 500
event, = events
assert (
event["transaction"]
== "tests.integrations.asgi.test_asgi.test_errors.<locals>.myerror"
)
exception, = event["exception"]["values"]
assert exception["type"] == "ValueError"
assert exception["value"] == "oh no"
assert any(
frame["filename"].endswith("tests/integrations/asgi/test_asgi.py")
for frame in exception["stacktrace"]["frames"]
)

View File

@ -0,0 +1,227 @@
import base64
import json
import os
import shutil
import subprocess
import sys
import uuid
from textwrap import dedent
import pytest
boto3 = pytest.importorskip("boto3")
LAMBDA_PRELUDE = """
from __future__ import print_function
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
import sentry_sdk
import json
from sentry_sdk.transport import Transport
class TestTransport(Transport):
def __init__(self):
Transport.__init__(self)
self._queue = []
def capture_event(self, event):
self._queue.append(event)
def flush(self, timeout, callback=None):
# Delay event output like this to test proper shutdown
# Note that AWS Lambda trunchates the log output to 4kb, so you better
# pray that your events are smaller than that or else tests start
# failing.
for event in self._queue:
print("EVENT:", json.dumps(event))
del self._queue[:]
def init_sdk(**extra_init_args):
sentry_sdk.init(
transport=TestTransport(),
integrations=[AwsLambdaIntegration()],
**extra_init_args
)
"""
@pytest.fixture
def lambda_client():
if "SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID" not in os.environ:
pytest.skip("AWS environ vars not set")
return boto3.client(
"lambda",
aws_access_key_id=os.environ["SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY"],
region_name="us-east-1",
)
@pytest.fixture(params=["python3.6", "python3.7", "python2.7"])
def run_lambda_function(tmpdir, lambda_client, request, semaphore_normalize):
def inner(code, payload):
runtime = request.param
tmpdir.ensure_dir("lambda_tmp").remove()
tmp = tmpdir.ensure_dir("lambda_tmp")
tmp.join("test_lambda.py").write(code)
# Check file for valid syntax first, and that the integration does not
# crash when not running in Lambda (but rather a local deployment tool
# such as chalice's)
subprocess.check_call([sys.executable, str(tmp.join("test_lambda.py"))])
tmp.join("setup.cfg").write("[install]\nprefix=")
subprocess.check_call([sys.executable, "setup.py", "sdist", "-d", str(tmpdir)])
# https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html
subprocess.check_call("pip install ../*.tar.gz -t .", cwd=str(tmp), shell=True)
shutil.make_archive(tmpdir.join("ball"), "zip", str(tmp))
fn_name = "test_function_{}".format(uuid.uuid4())
lambda_client.create_function(
FunctionName=fn_name,
Runtime=runtime,
Role=os.environ["SENTRY_PYTHON_TEST_AWS_IAM_ROLE"],
Handler="test_lambda.test_handler",
Code={"ZipFile": tmpdir.join("ball.zip").read(mode="rb")},
Description="Created as part of testsuite for getsentry/sentry-python",
)
@request.addfinalizer
def delete_function():
lambda_client.delete_function(FunctionName=fn_name)
response = lambda_client.invoke(
FunctionName=fn_name,
InvocationType="RequestResponse",
LogType="Tail",
Payload=payload,
)
assert 200 <= response["StatusCode"] < 300, response
events = []
for line in base64.b64decode(response["LogResult"]).splitlines():
print("AWS:", line)
if not line.startswith(b"EVENT: "):
continue
line = line[len(b"EVENT: ") :]
events.append(json.loads(line.decode("utf-8")))
semaphore_normalize(events[-1])
return events, response
return inner
def test_basic(run_lambda_function):
events, response = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk()
def test_handler(event, context):
raise Exception("something went wrong")
"""
),
b'{"foo": "bar"}',
)
assert response["FunctionError"] == "Unhandled"
event, = events
assert event["level"] == "error"
exception, = event["exception"]["values"]
assert exception["type"] == "Exception"
assert exception["value"] == "something went wrong"
frame1, = exception["stacktrace"]["frames"]
assert frame1["filename"] == "test_lambda.py"
assert frame1["abs_path"] == "/var/task/test_lambda.py"
assert frame1["function"] == "test_handler"
assert frame1["in_app"] is True
assert exception["mechanism"] == {"type": "aws_lambda", "handled": False}
assert event["extra"]["lambda"]["function_name"].startswith("test_function_")
def test_initialization_order(run_lambda_function):
"""Zappa lazily imports our code, so by the time we monkeypatch the handler
as seen by AWS already runs. At this point at least draining the queue
should work."""
events, _response = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
def test_handler(event, context):
init_sdk()
sentry_sdk.capture_exception(Exception("something went wrong"))
"""
),
b'{"foo": "bar"}',
)
event, = events
assert event["level"] == "error"
exception, = event["exception"]["values"]
assert exception["type"] == "Exception"
assert exception["value"] == "something went wrong"
def test_request_data(run_lambda_function):
events, _response = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk()
def test_handler(event, context):
sentry_sdk.capture_message("hi")
return "ok"
"""
),
payload=b"""
{
"resource": "/asd",
"path": "/asd",
"httpMethod": "GET",
"headers": {
"Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:62.0) Gecko/20100101 Firefox/62.0",
"X-Forwarded-Proto": "https"
},
"queryStringParameters": {
"bonkers": "true"
},
"pathParameters": null,
"stageVariables": null,
"requestContext": {
"identity": {
"sourceIp": "213.47.147.207",
"userArn": "42"
}
},
"body": null,
"isBase64Encoded": false
}
""",
)
event, = events
assert event["request"] == {
"headers": {
"Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:62.0) Gecko/20100101 Firefox/62.0",
"X-Forwarded-Proto": "https",
},
"method": "GET",
"query_string": {"bonkers": "true"},
"url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd",
}

View File

@ -0,0 +1,203 @@
import pytest
import inspect
pytest.importorskip("apache_beam")
import dill
from sentry_sdk.integrations.beam import (
BeamIntegration,
_wrap_task_call,
_wrap_inspect_call,
)
from apache_beam.typehints.trivial_inference import instance_to_type
from apache_beam.typehints.decorators import getcallargs_forhints
from apache_beam.transforms.core import DoFn, ParDo, _DoFnParam, CallableWrapperDoFn
from apache_beam.runners.common import DoFnInvoker, OutputProcessor, DoFnContext
from apache_beam.utils.windowed_value import WindowedValue
def foo():
return True
def bar(x, y):
# print(x + y)
return True
def baz(x, y=2):
# print(x + y)
return True
class A:
def __init__(self, fn):
self.r = "We are in A"
self.fn = fn
setattr(self, "_inspect_fn", _wrap_inspect_call(self, "fn"))
def process(self):
return self.fn()
class B(A, object):
def fa(self, x, element=False, another_element=False):
if x or (element and not another_element):
# print(self.r)
return True
1 / 0
return False
def __init__(self):
self.r = "We are in B"
super(B, self).__init__(self.fa)
class SimpleFunc(DoFn):
def process(self, x):
if x:
1 / 0
return [True]
class PlaceHolderFunc(DoFn):
def process(self, x, timestamp=DoFn.TimestampParam, wx=DoFn.WindowParam):
if isinstance(timestamp, _DoFnParam) or isinstance(wx, _DoFnParam):
raise Exception("Bad instance")
if x:
1 / 0
yield True
def fail(x):
if x:
1 / 0
return [True]
test_parent = A(foo)
test_child = B()
test_simple = SimpleFunc()
test_place_holder = PlaceHolderFunc()
test_callable = CallableWrapperDoFn(fail)
# Cannot call simple functions or placeholder test.
@pytest.mark.parametrize(
"obj,f,args,kwargs",
[
[test_parent, "fn", (), {}],
[test_child, "fn", (False,), {"element": True}],
[test_child, "fn", (True,), {}],
[test_simple, "process", (False,), {}],
[test_callable, "process", (False,), {}],
],
)
def test_monkey_patch_call(obj, f, args, kwargs):
func = getattr(obj, f)
assert func(*args, **kwargs)
assert _wrap_task_call(func)(*args, **kwargs)
@pytest.mark.parametrize("f", [foo, bar, baz, test_parent.fn, test_child.fn])
def test_monkey_patch_pickle(f):
f_temp = _wrap_task_call(f)
assert dill.pickles(f_temp), "{} is not pickling correctly!".format(f)
# Pickle everything
s1 = dill.dumps(f_temp)
s2 = dill.loads(s1)
dill.dumps(s2)
@pytest.mark.parametrize(
"f,args,kwargs",
[
[foo, (), {}],
[bar, (1, 5), {}],
[baz, (1,), {}],
[test_parent.fn, (), {}],
[test_child.fn, (False,), {"element": True}],
[test_child.fn, (True,), {}],
],
)
def test_monkey_patch_signature(f, args, kwargs):
arg_types = [instance_to_type(v) for v in args]
kwargs_types = {k: instance_to_type(v) for (k, v) in kwargs.items()}
f_temp = _wrap_task_call(f)
try:
getcallargs_forhints(f, *arg_types, **kwargs_types)
except Exception:
print("Failed on {} with parameters {}, {}".format(f, args, kwargs))
raise
try:
getcallargs_forhints(f_temp, *arg_types, **kwargs_types)
except Exception:
print("Failed on {} with parameters {}, {}".format(f_temp, args, kwargs))
raise
try:
expected_signature = inspect.signature(f)
test_signature = inspect.signature(f_temp)
assert (
expected_signature == test_signature
), "Failed on {}, signature {} does not match {}".format(
f, expected_signature, test_signature
)
except Exception:
# expected to pass for py2.7
pass
class _OutputProcessor(OutputProcessor):
def process_outputs(self, windowed_input_element, results):
print(windowed_input_element)
try:
for result in results:
assert result
except StopIteration:
print("In here")
@pytest.fixture
def init_beam(sentry_init):
def inner(fn):
sentry_init(default_integrations=False, integrations=[BeamIntegration()])
# Little hack to avoid having to run the whole pipeline.
pardo = ParDo(fn)
signature = pardo._signature
output_processor = _OutputProcessor()
return DoFnInvoker.create_invoker(
signature, output_processor, DoFnContext("test")
)
return inner
@pytest.mark.parametrize("fn", [test_simple, test_callable, test_place_holder])
def test_invoker_normal(init_beam, fn):
invoker = init_beam(fn)
print("Normal testing {} with {} invoker.".format(fn, invoker))
windowed_value = WindowedValue(False, 0, [None])
invoker.invoke_process(windowed_value)
@pytest.mark.parametrize("fn", [test_simple, test_callable, test_place_holder])
def test_invoker_exception(init_beam, capture_events, capture_exceptions, fn):
invoker = init_beam(fn)
events = capture_events()
print("Exception testing {} with {} invoker.".format(fn, invoker))
# Window value will always have one value for the process to run.
windowed_value = WindowedValue(True, 0, [None])
try:
invoker.invoke_process(windowed_value)
except Exception:
pass
event, = events
exception, = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
assert exception["mechanism"]["type"] == "beam"

View File

@ -0,0 +1,444 @@
import json
import pytest
import logging
pytest.importorskip("bottle")
from io import BytesIO
from bottle import Bottle, debug as set_debug, abort, redirect
from sentry_sdk import capture_message
from sentry_sdk.integrations.logging import LoggingIntegration
from werkzeug.test import Client
import sentry_sdk.integrations.bottle as bottle_sentry
@pytest.fixture(scope="function")
def app(sentry_init):
app = Bottle()
@app.route("/message")
def hi():
capture_message("hi")
return "ok"
@app.route("/message-named-route", name="hi")
def named_hi():
capture_message("hi")
return "ok"
yield app
@pytest.fixture
def get_client(app):
def inner():
return Client(app)
return inner
def test_has_context(sentry_init, app, capture_events, get_client):
sentry_init(integrations=[bottle_sentry.BottleIntegration()])
events = capture_events()
client = get_client()
response = client.get("/message")
assert response[1] == "200 OK"
event, = events
assert event["message"] == "hi"
assert "data" not in event["request"]
assert event["request"]["url"] == "http://localhost/message"
@pytest.mark.parametrize(
"url,transaction_style,expected_transaction",
[
("/message", "endpoint", "hi"),
("/message", "url", "/message"),
("/message-named-route", "endpoint", "hi"),
],
)
def test_transaction_style(
sentry_init,
app,
capture_events,
transaction_style,
expected_transaction,
url,
get_client,
):
sentry_init(
integrations=[
bottle_sentry.BottleIntegration(transaction_style=transaction_style)
]
)
events = capture_events()
client = get_client()
response = client.get("/message")
assert response[1] == "200 OK"
event, = events
assert event["transaction"].endswith(expected_transaction)
@pytest.mark.parametrize("debug", (True, False), ids=["debug", "nodebug"])
@pytest.mark.parametrize("catchall", (True, False), ids=["catchall", "nocatchall"])
def test_errors(
sentry_init, capture_exceptions, capture_events, app, debug, catchall, get_client
):
sentry_init(integrations=[bottle_sentry.BottleIntegration()])
app.catchall = catchall
set_debug(mode=debug)
exceptions = capture_exceptions()
events = capture_events()
@app.route("/")
def index():
1 / 0
client = get_client()
try:
client.get("/")
except ZeroDivisionError:
pass
exc, = exceptions
assert isinstance(exc, ZeroDivisionError)
event, = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "bottle"
assert event["exception"]["values"][0]["mechanism"]["handled"] is False
def test_large_json_request(sentry_init, capture_events, app, get_client):
sentry_init(integrations=[bottle_sentry.BottleIntegration()])
data = {"foo": {"bar": "a" * 2000}}
@app.route("/", method="POST")
def index():
import bottle
assert bottle.request.json == data
assert bottle.request.body.read() == json.dumps(data).encode("ascii")
capture_message("hi")
return "ok"
events = capture_events()
client = get_client()
response = client.get("/")
response = client.post("/", content_type="application/json", data=json.dumps(data))
assert response[1] == "200 OK"
event, = events
# __import__("pdb").set_trace()
assert event["_meta"]["request"]["data"]["foo"]["bar"] == {
"": {"len": 2000, "rem": [["!limit", "x", 509, 512]]}
}
assert len(event["request"]["data"]["foo"]["bar"]) == 512
@pytest.mark.parametrize("data", [{}, []], ids=["empty-dict", "empty-list"])
def test_empty_json_request(sentry_init, capture_events, app, data, get_client):
sentry_init(integrations=[bottle_sentry.BottleIntegration()])
@app.route("/", method="POST")
def index():
import bottle
assert bottle.request.json == data
assert bottle.request.body.read() == json.dumps(data).encode("ascii")
# assert not bottle.request.forms
capture_message("hi")
return "ok"
events = capture_events()
client = get_client()
response = client.post("/", content_type="application/json", data=json.dumps(data))
assert response[1] == "200 OK"
event, = events
assert event["request"]["data"] == data
def test_medium_formdata_request(sentry_init, capture_events, app, get_client):
sentry_init(integrations=[bottle_sentry.BottleIntegration()])
data = {"foo": "a" * 2000}
@app.route("/", method="POST")
def index():
import bottle
assert bottle.request.forms["foo"] == data["foo"]
capture_message("hi")
return "ok"
events = capture_events()
client = get_client()
response = client.post("/", data=data)
assert response[1] == "200 OK"
event, = events
assert event["_meta"]["request"]["data"]["foo"] == {
"": {"len": 2000, "rem": [["!limit", "x", 509, 512]]}
}
assert len(event["request"]["data"]["foo"]) == 512
@pytest.mark.parametrize("input_char", [u"a", b"a"])
def test_too_large_raw_request(
sentry_init, input_char, capture_events, app, get_client
):
sentry_init(
integrations=[bottle_sentry.BottleIntegration()], request_bodies="small"
)
data = input_char * 2000
@app.route("/", method="POST")
def index():
import bottle
if isinstance(data, bytes):
assert bottle.request.body.read() == data
else:
assert bottle.request.body.read() == data.encode("ascii")
assert not bottle.request.json
capture_message("hi")
return "ok"
events = capture_events()
client = get_client()
response = client.post("/", data=data)
assert response[1] == "200 OK"
event, = events
assert event["_meta"]["request"]["data"] == {
"": {"len": 2000, "rem": [["!config", "x", 0, 2000]]}
}
assert not event["request"]["data"]
def test_files_and_form(sentry_init, capture_events, app, get_client):
sentry_init(
integrations=[bottle_sentry.BottleIntegration()], request_bodies="always"
)
data = {"foo": "a" * 2000, "file": (BytesIO(b"hello"), "hello.txt")}
@app.route("/", method="POST")
def index():
import bottle
assert list(bottle.request.forms) == ["foo"]
assert list(bottle.request.files) == ["file"]
assert not bottle.request.json
capture_message("hi")
return "ok"
events = capture_events()
client = get_client()
response = client.post("/", data=data)
assert response[1] == "200 OK"
event, = events
assert event["_meta"]["request"]["data"]["foo"] == {
"": {"len": 2000, "rem": [["!limit", "x", 509, 512]]}
}
assert len(event["request"]["data"]["foo"]) == 512
assert event["_meta"]["request"]["data"]["file"] == {
"": {
"len": -1,
"rem": [["!raw", "x", 0, -1]],
} # bottle default content-length is -1
}
assert not event["request"]["data"]["file"]
@pytest.mark.parametrize(
"integrations",
[
[bottle_sentry.BottleIntegration()],
[bottle_sentry.BottleIntegration(), LoggingIntegration(event_level="ERROR")],
],
)
def test_errors_not_reported_twice(
sentry_init, integrations, capture_events, app, get_client
):
sentry_init(integrations=integrations)
app.catchall = False
logger = logging.getLogger("bottle.app")
@app.route("/")
def index():
try:
1 / 0
except Exception as e:
logger.exception(e)
raise e
events = capture_events()
client = get_client()
with pytest.raises(ZeroDivisionError):
client.get("/")
assert len(events) == 1
def test_logging(sentry_init, capture_events, app, get_client):
# ensure that Bottle's logger magic doesn't break ours
sentry_init(
integrations=[
bottle_sentry.BottleIntegration(),
LoggingIntegration(event_level="ERROR"),
]
)
@app.route("/")
def index():
app.logger.error("hi")
return "ok"
events = capture_events()
client = get_client()
client.get("/")
event, = events
assert event["level"] == "error"
def test_mount(app, capture_exceptions, capture_events, sentry_init, get_client):
sentry_init(integrations=[bottle_sentry.BottleIntegration()])
app.catchall = False
def crashing_app(environ, start_response):
1 / 0
app.mount("/wsgi/", crashing_app)
client = Client(app)
exceptions = capture_exceptions()
events = capture_events()
with pytest.raises(ZeroDivisionError) as exc:
client.get("/wsgi/")
error, = exceptions
assert error is exc.value
event, = events
assert event["exception"]["values"][0]["mechanism"] == {
"type": "bottle",
"handled": False,
}
def test_500(sentry_init, capture_events, app, get_client):
sentry_init(integrations=[bottle_sentry.BottleIntegration()])
set_debug(False)
app.catchall = True
@app.route("/")
def index():
1 / 0
@app.error(500)
def error_handler(err):
capture_message("error_msg")
return "My error"
events = capture_events()
client = get_client()
response = client.get("/")
assert response[1] == "500 Internal Server Error"
_, event = events
assert event["message"] == "error_msg"
def test_error_in_errorhandler(sentry_init, capture_events, app, get_client):
sentry_init(integrations=[bottle_sentry.BottleIntegration()])
set_debug(False)
app.catchall = True
@app.route("/")
def index():
raise ValueError()
@app.error(500)
def error_handler(err):
1 / 0
events = capture_events()
client = get_client()
with pytest.raises(ZeroDivisionError):
client.get("/")
event1, event2 = events
exception, = event1["exception"]["values"]
assert exception["type"] == "ValueError"
exception = event2["exception"]["values"][0]
assert exception["type"] == "ZeroDivisionError"
def test_bad_request_not_captured(sentry_init, capture_events, app, get_client):
sentry_init(integrations=[bottle_sentry.BottleIntegration()])
events = capture_events()
@app.route("/")
def index():
abort(400, "bad request in")
client = get_client()
client.get("/")
assert not events
def test_no_exception_on_redirect(sentry_init, capture_events, app, get_client):
sentry_init(integrations=[bottle_sentry.BottleIntegration()])
events = capture_events()
@app.route("/")
def index():
redirect("/here")
@app.route("/here")
def here():
return "here"
client = get_client()
client.get("/")
assert not events

View File

@ -0,0 +1,311 @@
import threading
import pytest
pytest.importorskip("celery")
from sentry_sdk import Hub, configure_scope
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk._compat import text_type
from celery import Celery, VERSION
from celery.bin import worker
@pytest.fixture
def connect_signal(request):
def inner(signal, f):
signal.connect(f)
request.addfinalizer(lambda: signal.disconnect(f))
return inner
@pytest.fixture
def init_celery(sentry_init):
def inner(propagate_traces=True, **kwargs):
sentry_init(
integrations=[CeleryIntegration(propagate_traces=propagate_traces)],
**kwargs
)
celery = Celery(__name__)
if VERSION < (4,):
celery.conf.CELERY_ALWAYS_EAGER = True
else:
celery.conf.task_always_eager = True
return celery
return inner
@pytest.fixture
def celery(init_celery):
return init_celery()
@pytest.fixture(
params=[
lambda task, x, y: (task.delay(x, y), {"args": [x, y], "kwargs": {}}),
lambda task, x, y: (task.apply_async((x, y)), {"args": [x, y], "kwargs": {}}),
lambda task, x, y: (
task.apply_async(args=(x, y)),
{"args": [x, y], "kwargs": {}},
),
lambda task, x, y: (
task.apply_async(kwargs=dict(x=x, y=y)),
{"args": [], "kwargs": {"x": x, "y": y}},
),
]
)
def celery_invocation(request):
"""
Invokes a task in multiple ways Celery allows you to (testing our apply_async monkeypatch).
Currently limited to a task signature of the form foo(x, y)
"""
return request.param
def test_simple(capture_events, celery, celery_invocation):
events = capture_events()
@celery.task(name="dummy_task")
def dummy_task(x, y):
foo = 42 # noqa
return x / y
with Hub.current.start_span() as span:
celery_invocation(dummy_task, 1, 2)
_, expected_context = celery_invocation(dummy_task, 1, 0)
event, = events
assert event["contexts"]["trace"]["trace_id"] == span.trace_id
assert event["contexts"]["trace"]["span_id"] != span.span_id
assert event["transaction"] == "dummy_task"
assert event["extra"]["celery-job"] == dict(
task_name="dummy_task", **expected_context
)
exception, = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
assert exception["mechanism"]["type"] == "celery"
assert exception["stacktrace"]["frames"][0]["vars"]["foo"] == "42"
@pytest.mark.parametrize("task_fails", [True, False], ids=["error", "success"])
def test_transaction_events(capture_events, init_celery, celery_invocation, task_fails):
celery = init_celery(traces_sample_rate=1.0)
@celery.task(name="dummy_task")
def dummy_task(x, y):
return x / y
# XXX: For some reason the first call does not get instrumented properly.
celery_invocation(dummy_task, 1, 1)
events = capture_events()
with Hub.current.start_span(transaction="submission") as span:
celery_invocation(dummy_task, 1, 0 if task_fails else 1)
if task_fails:
error_event = events.pop(0)
assert error_event["contexts"]["trace"]["trace_id"] == span.trace_id
assert error_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
execution_event, submission_event = events
assert execution_event["transaction"] == "dummy_task"
assert submission_event["transaction"] == "submission"
assert execution_event["type"] == submission_event["type"] == "transaction"
assert execution_event["contexts"]["trace"]["trace_id"] == span.trace_id
assert submission_event["contexts"]["trace"]["trace_id"] == span.trace_id
if task_fails:
assert execution_event["contexts"]["trace"]["status"] == "failure"
else:
assert "status" not in execution_event["contexts"]["trace"]
assert execution_event["spans"] == []
assert submission_event["spans"] == [
{
u"data": {},
u"description": u"dummy_task",
u"op": "celery.submit",
u"parent_span_id": submission_event["contexts"]["trace"]["span_id"],
u"same_process_as_parent": True,
u"span_id": submission_event["spans"][0]["span_id"],
u"start_timestamp": submission_event["spans"][0]["start_timestamp"],
u"tags": {},
u"timestamp": submission_event["spans"][0]["timestamp"],
u"trace_id": text_type(span.trace_id),
}
]
def test_no_stackoverflows(celery):
"""We used to have a bug in the Celery integration where its monkeypatching
was repeated for every task invocation, leading to stackoverflows.
See https://github.com/getsentry/sentry-python/issues/265
"""
results = []
@celery.task(name="dummy_task")
def dummy_task():
with configure_scope() as scope:
scope.set_tag("foo", "bar")
results.append(42)
for _ in range(10000):
dummy_task.delay()
assert results == [42] * 10000
with configure_scope() as scope:
assert not scope._tags
def test_simple_no_propagation(capture_events, init_celery):
celery = init_celery(propagate_traces=False)
events = capture_events()
@celery.task(name="dummy_task")
def dummy_task():
1 / 0
with Hub.current.start_span() as span:
dummy_task.delay()
event, = events
assert event["contexts"]["trace"]["trace_id"] != span.trace_id
assert event["transaction"] == "dummy_task"
exception, = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
def test_ignore_expected(capture_events, celery):
events = capture_events()
@celery.task(name="dummy_task", throws=(ZeroDivisionError,))
def dummy_task(x, y):
return x / y
dummy_task.delay(1, 2)
dummy_task.delay(1, 0)
assert not events
def test_broken_prerun(init_celery, connect_signal):
from celery.signals import task_prerun
stack_lengths = []
def crash(*args, **kwargs):
# scope should exist in prerun
stack_lengths.append(len(Hub.current._stack))
1 / 0
# Order here is important to reproduce the bug: In Celery 3, a crashing
# prerun would prevent other preruns from running.
connect_signal(task_prerun, crash)
celery = init_celery()
assert len(Hub.current._stack) == 1
@celery.task(name="dummy_task")
def dummy_task(x, y):
stack_lengths.append(len(Hub.current._stack))
return x / y
if VERSION >= (4,):
dummy_task.delay(2, 2)
else:
with pytest.raises(ZeroDivisionError):
dummy_task.delay(2, 2)
assert len(Hub.current._stack) == 1
if VERSION < (4,):
assert stack_lengths == [2]
else:
assert stack_lengths == [2, 2]
@pytest.mark.xfail(
(4, 2, 0) <= VERSION,
strict=True,
reason="https://github.com/celery/celery/issues/4661",
)
def test_retry(celery, capture_events):
events = capture_events()
failures = [True, True, False]
runs = []
@celery.task(name="dummy_task", bind=True)
def dummy_task(self):
runs.append(1)
try:
if failures.pop(0):
1 / 0
except Exception as exc:
self.retry(max_retries=2, exc=exc)
dummy_task.delay()
assert len(runs) == 3
assert not events
failures = [True, True, True]
runs = []
dummy_task.delay()
assert len(runs) == 3
event, = events
exceptions = event["exception"]["values"]
for e in exceptions:
assert e["type"] == "ZeroDivisionError"
@pytest.mark.skipif(VERSION < (4,), reason="in-memory backend broken")
def test_transport_shutdown(request, celery, capture_events_forksafe, tmpdir):
events = capture_events_forksafe()
celery.conf.worker_max_tasks_per_child = 1
celery.conf.broker_url = "memory://localhost/"
celery.conf.broker_backend = "memory"
celery.conf.result_backend = "file://{}".format(tmpdir.mkdir("celery-results"))
celery.conf.task_always_eager = False
runs = []
@celery.task(name="dummy_task", bind=True)
def dummy_task(self):
runs.append(1)
1 / 0
res = dummy_task.delay()
w = worker.worker(app=celery)
t = threading.Thread(target=w.run)
t.daemon = True
t.start()
with pytest.raises(Exception):
# Celery 4.1 raises a gibberish exception
res.wait()
event = events.read_event()
exception, = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
events.read_flush()
# if this is nonempty, the worker never really forked
assert not runs

View File

@ -0,0 +1,21 @@
import pytest
import sentry_sdk
@pytest.fixture
def capture_exceptions(monkeypatch):
def inner():
errors = set()
old_capture_event = sentry_sdk.Hub.capture_event
def capture_event(self, event, hint=None):
if hint:
if "exc_info" in hint:
error = hint["exc_info"][1]
errors.add(error)
return old_capture_event(self, event, hint=hint)
monkeypatch.setattr(sentry_sdk.Hub, "capture_event", capture_event)
return errors
return inner

View File

@ -0,0 +1,3 @@
import pytest
django = pytest.importorskip("django")

View File

@ -0,0 +1,3 @@
import pytest
pytest.importorskip("channels")

View File

@ -0,0 +1,39 @@
import pytest
from channels.testing import HttpCommunicator
from sentry_sdk import capture_message
from sentry_sdk.integrations.django import DjangoIntegration
from tests.integrations.django.myapp.asgi import application
@pytest.mark.asyncio
async def test_basic(sentry_init, capture_events):
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
events = capture_events()
comm = HttpCommunicator(application, "GET", "/view-exc?test=query")
response = await comm.get_response()
assert response["status"] == 500
event, = events
exception, = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
# Test that the ASGI middleware got set up correctly. Right now this needs
# to be installed manually (see myapp/asgi.py)
assert event["transaction"] == "/view-exc"
assert event["request"] == {
"cookies": {},
"headers": {},
"method": "GET",
"query_string": "test=query",
"url": "/view-exc",
}
capture_message("hi")
event = events[-1]
assert "request" not in event

View File

@ -0,0 +1,15 @@
"""
ASGI entrypoint. Configures Django and then runs the application
defined in the ASGI_APPLICATION setting.
"""
import os
import django
from channels.routing import get_default_application
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE", "tests.integrations.django.myapp.settings"
)
django.setup()
application = get_default_application()

View File

@ -0,0 +1,12 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE", "tests.integrations.django.myapp.settings"
)
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)

Some files were not shown because too many files have changed in this diff Show More