def test_empty_measurement_interface(mini_sentry, relay_chain): # set up relay relay = relay_chain() mini_sentry.add_basic_project_config(42) # construct envelope transaction_item = generate_transaction_item() transaction_item.update({"measurements": {}}) envelope = Envelope() envelope.add_transaction(transaction_item) # ingest envelope relay.send_envelope(42, envelope) envelope = mini_sentry.captured_events.get(timeout=1) event = envelope.get_transaction_event() # test actual output assert event["transaction"] == "/organizations/:orgId/performance/:eventSlug/" assert "measurements" not in event, event
def test_it_removes_transactions(mini_sentry, relay): """ Tests that when sampling is set to 0% for the trace context project the transactions are removed """ project_id = 42 relay = relay(mini_sentry, _outcomes_enabled_config()) # create a basic project config config = mini_sentry.add_basic_project_config(project_id) # add a sampling rule to project config that removes all transactions (sample_rate=0) public_key = config["publicKeys"][0]["publicKey"] _add_sampling_config(config, project_ids=[project_id], sample_rate=0) # create an envelope with a trace context that is initiated by this project (for simplicity) envelope = Envelope() transaction, trace_id = _create_transaction_item() envelope.add_transaction(transaction) _add_trace_info(envelope, trace_id=trace_id, public_key=public_key) # send the event, the transaction should be removed. relay.send_envelope(project_id, envelope) # the event should be removed by Relay sampling with pytest.raises(queue.Empty): mini_sentry.captured_events.get(timeout=1) outcomes = mini_sentry.captured_outcomes.get(timeout=2) assert outcomes is not None outcome = outcomes["outcomes"][0] assert outcome.get("outcome") == 3 assert outcome.get("reason") == "transaction_sampled"
def inner(user): transaction_data = generator() project_info = get_project_info(user) envelope = Envelope() envelope.add_transaction(transaction_data) return send_envelope(user.client, project_info.id, project_info.key, envelope)
def test_fast_path(mini_sentry, relay): """ Tests that the fast path works. When the project config is already fetched and the envelope only contains a transaction a fast evaluation path is used. While this is an implementation detail of Relay that is not visible here we test that Relay behaves normally for the conditions that we know are going to trigger a fast-path evaluation """ project_id = 42 relay = relay(mini_sentry, _outcomes_enabled_config()) # create a basic project config config = mini_sentry.add_basic_project_config(project_id) # add a sampling rule to project config that removes all transactions (sample_rate=0) public_key = config["publicKeys"][0]["publicKey"] _add_sampling_config(config, project_ids=[project_id], sample_rate=0) for i in range(2): # create an envelope with a trace context that is initiated by this project (for simplicity) envelope = Envelope() transaction, trace_id = _create_transaction_item() envelope.add_transaction(transaction) _add_trace_info(envelope, trace_id=trace_id, public_key=public_key) # send the event, the transaction should be removed. relay.send_envelope(project_id, envelope) # the event should be removed by Relay sampling with pytest.raises(queue.Empty): mini_sentry.captured_events.get(timeout=1) outcomes = mini_sentry.captured_outcomes.get(timeout=2) assert outcomes is not None
def test_it_keeps_transactions(mini_sentry, relay): """ Tests that when sampling is set to 100% for the trace context project the transactions are kept """ project_id = 42 relay = relay(mini_sentry, _outcomes_enabled_config()) # create a basic project config config = mini_sentry.add_basic_project_config(project_id) # add a sampling rule to project config that keeps all transactions (sample_rate=1) public_key = config["publicKeys"][0]["publicKey"] _add_sampling_config(config, project_ids=[project_id], sample_rate=1) # create an envelope with a trace context that is initiated by this project (for simplicity) envelope = Envelope() transaction, trace_id = _create_transaction_item() envelope.add_transaction(transaction) _add_trace_info(envelope, trace_id=trace_id, public_key=public_key) # send the event, the transaction should be removed. relay.send_envelope(project_id, envelope) # the event should be left alone by Relay sampling evt = mini_sentry.captured_events.get(timeout=1).get_transaction_event() assert evt is not None # double check that we get back our trace object (check the trace_id from the object) evt_trace_id = (evt.setdefault("contexts", {}).setdefault("trace", {}).get("trace_id")) assert evt_trace_id == trace_id # no outcome should be generated since we forward the event to upstream with pytest.raises(queue.Empty): mini_sentry.captured_outcomes.get(timeout=2)
def test_normalize_measurement_interface(mini_sentry, relay_with_processing, transactions_consumer): relay = relay_with_processing() mini_sentry.add_basic_project_config(42) events_consumer = transactions_consumer() transaction_item = generate_transaction_item() transaction_item.update({ "measurements": { "LCP": { "value": 420.69 }, " lcp_final.element-Size123 ": { "value": 1 }, "fid": { "value": 2020 }, "cls": { "value": None }, "fp": { "value": "im a first paint" }, "Total Blocking Time": { "value": 3.14159 }, "missing_value": "string", } }) envelope = Envelope() envelope.add_transaction(transaction_item) relay.send_envelope(42, envelope) event, _ = events_consumer.get_event() assert event[ "transaction"] == "/organizations/:orgId/performance/:eventSlug/" assert "trace" in event["contexts"] assert "measurements" in event, event assert event["measurements"] == { "lcp": { "value": 420.69 }, "lcp_final.element-size123": { "value": 1 }, "fid": { "value": 2020 }, "cls": { "value": None }, "fp": { "value": None }, "missing_value": None, }
def test_empty_span_attributes( mini_sentry, relay_with_processing, transactions_consumer ): events_consumer = transactions_consumer() relay = relay_with_processing() config = mini_sentry.add_basic_project_config(42) config["config"].setdefault("spanAttributes", []) transaction_item = generate_transaction_item() transaction_item.update( { "spans": [ { "description": "GET /api/0/organizations/?member=1", "op": "http", "parent_span_id": "aaaaaaaaaaaaaaaa", "span_id": "bbbbbbbbbbbbbbbb", "start_timestamp": 1000, "timestamp": 3000, "trace_id": "ff62a8b040f340bda5d830223def1d81", }, { "description": "GET /api/0/organizations/?member=1", "op": "http", "parent_span_id": "bbbbbbbbbbbbbbbb", "span_id": "cccccccccccccccc", "start_timestamp": 1400, "timestamp": 2600, "trace_id": "ff62a8b040f340bda5d830223def1d81", }, { "description": "GET /api/0/organizations/?member=1", "op": "http", "parent_span_id": "cccccccccccccccc", "span_id": "dddddddddddddddd", "start_timestamp": 1700, "timestamp": 2300, "trace_id": "ff62a8b040f340bda5d830223def1d81", }, ], } ) envelope = Envelope() envelope.add_transaction(transaction_item) relay.send_envelope(42, envelope) event, _ = events_consumer.get_event() assert event["transaction"] == "/organizations/:orgId/performance/:eventSlug/" assert "trace" in event["contexts"] assert "exclusive_time" not in event["contexts"]["trace"] for span in event["spans"]: assert "exclusive_time" not in span
def test_transaction_event(): envelope = Envelope() transaction_item = generate_transaction_item() transaction_item.update({"event_id": "a" * 32}) envelope.add_transaction(transaction_item) # typically it should not be possible to be able to add a second transaction; # but we do it anyways another_transaction_item = generate_transaction_item() envelope.add_transaction(another_transaction_item) # should only fetch the first inserted transaction event assert envelope.get_transaction_event() == transaction_item
def test_empty_measurement_interface(mini_sentry, relay_chain): relay = relay_chain(min_relay_version="20.10.0") mini_sentry.add_basic_project_config(42) transaction_item = generate_transaction_item() transaction_item.update({"measurements": {}}) envelope = Envelope() envelope.add_transaction(transaction_item) relay.send_envelope(42, envelope) envelope = mini_sentry.captured_events.get(timeout=1) event = envelope.get_transaction_event() assert event["transaction"] == "/organizations/:orgId/performance/:eventSlug/" assert "measurements" not in event, event
def send_transaction(self, project_id, payload, item_headers=None): envelope = Envelope() envelope.add_transaction(payload) if item_headers: item = envelope.items[0] item.headers = {**item.headers, **item_headers} if envelope.headers is None: envelope.headers = {} trace_info = { "trace_id": payload["contexts"]["trace"]["trace_id"], "public_key": self.get_dsn_public_key(project_id), } envelope.headers["trace"] = trace_info self.send_envelope(project_id, envelope)
def test_multi_item_envelope(mini_sentry, relay): """ Test that multi item envelopes are handled correctly. Pass an envelope containing of a transaction that will be sampled and an event and test """ project_id = 42 relay = relay(mini_sentry, _outcomes_enabled_config()) # create a basic project config config = mini_sentry.add_basic_project_config(project_id) # add a sampling rule to project config that removes all transactions (sample_rate=0) public_key = config["publicKeys"][0]["publicKey"] _add_sampling_config(config, project_ids=[project_id], sample_rate=0) # we'll run the test twice to make sure that the fast path works as well for i in range(2): # create an envelope with a trace context that is initiated by this project (for simplicity) envelope = Envelope() transaction, trace_id = _create_transaction_item() envelope.add_transaction(transaction) envelope.add_event({"message": "Hello"}) _add_trace_info(envelope, trace_id=trace_id, public_key=public_key) # send the event, the transaction should be removed. relay.send_envelope(project_id, envelope) msg = mini_sentry.captured_events.get(timeout=1) assert msg is not None transaction = msg.get_transaction_event() event = msg.get_event() # check that the transaction was removed during the transaction sampling process assert transaction is None # but the event was kept assert event is not None assert event.setdefault("logentry", {}).get("formatted") == "Hello" # no outcome should be generated since we forward the event to upstream # NOTE this might change in the future so we might get outcomes here in the future with pytest.raises(queue.Empty): mini_sentry.captured_outcomes.get(timeout=2)
def test_ops_breakdowns(mini_sentry, relay_with_processing, transactions_consumer): events_consumer = transactions_consumer() relay = relay_with_processing() config = mini_sentry.add_basic_project_config(42) config["config"].setdefault( "breakdownsV2", { "span_ops": { "type": "span_operations", "matches": ["http", "db", "browser", "resource"], }, "span_ops_2": { "type": "spanOperations", "matches": ["http", "db", "browser", "resource"], }, "span_ops_3": { "type": "whatever", "matches": ["http", "db", "browser", "resource"], }, }, ) config["config"].setdefault( # old name of span operation breakdown key. we expect these to not generate anything at all "breakdowns", { "span_ops_4": { "type": "span_operations", "matches": ["http", "db", "browser", "resource"], }, "span_ops_5": { "type": "spanOperations", "matches": ["http", "db", "browser", "resource"], }, "span_ops_6": { "type": "whatever", "matches": ["http", "db", "browser", "resource"], }, }, ) transaction_item = generate_transaction_item() transaction_item.update( { "spans": [ { "description": "GET /api/0/organizations/?member=1", "op": "http", "parent_span_id": "8f5a2b8768cafb4e", "span_id": "bd429c44b67a3eb4", "start_timestamp": 1000.5, "timestamp": 2000.5, "trace_id": "ff62a8b040f340bda5d830223def1d81", }, { "description": "GET /api/0/organizations/?member=1", "op": "http", "parent_span_id": "8f5a2b8768cafb4e", "span_id": "bd429c44b67a3eb5", "start_timestamp": 1500.5, "timestamp": 2500.5, "trace_id": "ff62a8b040f340bda5d830223def1d81", }, { "description": "GET /api/0/organizations/?member=1", "op": "http.secure", "parent_span_id": "8f5a2b8768cafb4e", "span_id": "bd429c44b67a3eb6", "start_timestamp": 3500, "timestamp": 4000, "trace_id": "ff62a8b040f340bda5d830223def1d81", }, { "description": "sentry-cdn.com/dist.js", "op": "resource.link", "parent_span_id": "8f5a2b8768cafb4f", "span_id": "bd429c44b67a3eb7", "start_timestamp": 500.001, "timestamp": 600.002003, "trace_id": "ff62a8b040f340bda5d830223def1d81", }, { "description": "sentry.middleware.env.SentryEnvMiddleware.process_request", "op": "django.middleware", "parent_span_id": "8f5a2b8768cafb4e", "span_id": "bd429c44b67a3eb8", "start_timestamp": 100, "timestamp": 200, "trace_id": "ff62a8b040f340bda5d830223def1d81", }, ], } ) envelope = Envelope() envelope.add_transaction(transaction_item) relay.send_envelope(42, envelope) event, _ = events_consumer.get_event() assert event["transaction"] == "/organizations/:orgId/performance/:eventSlug/" assert "trace" in event["contexts"] assert "breakdowns" in event, event assert event["breakdowns"] == { "span_ops": { "ops.http": {"value": 2000000.0, "unit": "millisecond"}, "ops.resource": {"value": 100001.003, "unit": "millisecond"}, "total.time": {"value": 2200001.003, "unit": "millisecond"}, }, "span_ops_2": { "ops.http": {"value": 2000000.0, "unit": "millisecond"}, "ops.resource": {"value": 100001.003, "unit": "millisecond"}, "total.time": {"value": 2200001.003, "unit": "millisecond"}, }, }
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. """ if disable_capture_event.get(False): return None if self.transport is None: return None if hint is None: hint = {} event_id = event.get("event_id") hint = dict(hint or ()) # type: Hint 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 # whenever we capture an event we also check if the session needs # to be updated based on that information. session = scope._session if scope else None if session: self._update_session_from_event(session, event) attachments = hint.get("attachments") is_transaction = event_opt.get("type") == "transaction" if is_transaction or attachments: # Transactions or events with attachments should go to the # /envelope/ endpoint. envelope = Envelope( headers={ "event_id": event_opt["event_id"], "sent_at": format_timestamp(datetime.utcnow()), }) if is_transaction: envelope.add_transaction(event_opt) else: envelope.add_event(event_opt) for attachment in attachments or (): envelope.add_item(attachment.to_envelope_item()) self.transport.capture_envelope(envelope) else: # All other events go to the /store/ endpoint. self.transport.capture_event(event_opt) return event_id
def test_uses_trace_public_key(mini_sentry, relay): """ Tests that the public_key from the trace context is used The project configuration corresponding to the project pointed to by the context public_key DSN is used (not the dsn of the request) Create a trace context for projectA and send an event from projectB using projectA's trace. Configure project1 to sample out all events (sample_rate=0) Configure project2 to sample in all events (sample_rate=1) First: Send event to project2 with trace from project1 It should be removed (sampled out) Second: Send event to project1 with trace from project2 It should pass through """ relay = relay(mini_sentry, _outcomes_enabled_config()) # create basic project configs project_id1 = 42 config1 = mini_sentry.add_basic_project_config(project_id1) public_key1 = config1["publicKeys"][0]["publicKey"] _add_sampling_config(config1, project_ids=[project_id1], sample_rate=0) project_id2 = 43 config2 = mini_sentry.add_basic_project_config(project_id2) public_key2 = config2["publicKeys"][0]["publicKey"] _add_sampling_config(config2, project_ids=[project_id1], sample_rate=1) # First # send trace with project_id1 context (should be removed) envelope = Envelope() transaction, trace_id = _create_transaction_item() envelope.add_transaction(transaction) _add_trace_info(envelope, trace_id=trace_id, public_key=public_key1) # send the event, the transaction should be removed. relay.send_envelope(project_id2, envelope) # the event should be removed by Relay sampling with pytest.raises(queue.Empty): mini_sentry.captured_events.get(timeout=1) # and it should create an outcome outcomes = mini_sentry.captured_outcomes.get(timeout=2) assert outcomes is not None # Second # send trace with project_id2 context (should go through) envelope = Envelope() transaction, trace_id = _create_transaction_item() envelope.add_transaction(transaction) _add_trace_info(envelope, trace_id=trace_id, public_key=public_key2) # send the event. relay.send_envelope(project_id1, envelope) # the event should be passed along to upstream (with the transaction unchanged) evt = mini_sentry.captured_events.get(timeout=1).get_transaction_event() assert evt is not None # no outcome should be generated (since the event is passed along to the upstream) with pytest.raises(queue.Empty): mini_sentry.captured_outcomes.get(timeout=2)
def _create_transaction_envelope(public_key): envelope = Envelope() transaction, trace_id, event_id = _create_transaction_item() envelope.add_transaction(transaction) _add_trace_info(envelope, trace_id=trace_id, public_key=public_key) return envelope, trace_id, event_id
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. """ if disable_capture_event.get(False): return None if self.transport is None: return None if hint is None: hint = {} event_id = event.get("event_id") hint = dict(hint or ()) # type: Hint 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 # whenever we capture an event we also check if the session needs # to be updated based on that information. session = scope._session if scope else None if session: self._update_session_from_event(session, event) is_transaction = event_opt.get("type") == "transaction" if not is_transaction and not self._should_sample_error(event): return None attachments = hint.get("attachments") # this is outside of the `if` immediately below because even if we don't # use the value, we want to make sure we remove it before the event is # sent raw_tracestate = (event_opt.get("contexts", {}).get("trace", {}).pop("tracestate", "")) # Transactions or events with attachments should go to the /envelope/ # endpoint. if is_transaction or attachments: headers = { "event_id": event_opt["event_id"], "sent_at": format_timestamp(datetime.utcnow()), } tracestate_data = raw_tracestate and reinflate_tracestate( raw_tracestate.replace("sentry=", "")) if tracestate_data and has_tracestate_enabled(): headers["trace"] = tracestate_data envelope = Envelope(headers=headers) if is_transaction: envelope.add_transaction(event_opt) else: envelope.add_event(event_opt) for attachment in attachments or (): envelope.add_item(attachment.to_envelope_item()) self.transport.capture_envelope(envelope) else: # All other events go to the /store/ endpoint. self.transport.capture_event(event_opt) return event_id