Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
name: E2E Tracing Tests

on:
push:
branches:
- master
- release/**
pull_request:

jobs:
tracing-e2e:
name: Tracing E2E Tests
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Setup Elixir and Erlang
uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9
with:
elixir-version: "1.18"
otp-version: "27.2"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Cache Elixir dependencies
uses: actions/cache@v4
with:
path: |
deps
_build
test_integrations/phoenix_app/deps
test_integrations/phoenix_app/_build
key: ${{ runner.os }}-elixir-1.18-otp-27.2-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-elixir-1.18-otp-27.2-mix-

- name: Cache Node.js dependencies
uses: actions/cache@v4
with:
path: |
test_integrations/tracing/node_modules
test_integrations/tracing/svelte_mini/node_modules
key: ${{ runner.os }}-node-20-${{ hashFiles('test_integrations/tracing/package-lock.json', 'test_integrations/tracing/svelte_mini/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-20-

- name: Install main project dependencies
run: mix deps.get

- name: Install Phoenix app dependencies
working-directory: test_integrations/phoenix_app
run: mix deps.get

- name: Compile Phoenix app
working-directory: test_integrations/phoenix_app
run: mix compile

- name: Install tracing test npm dependencies
working-directory: test_integrations/tracing
run: npm install

- name: Install Svelte app dependencies
working-directory: test_integrations/tracing/svelte_mini
run: npm install

- name: Install Playwright browsers
working-directory: test_integrations/tracing
run: npx playwright install --with-deps --only-shell chromium

- name: Start Phoenix server
working-directory: test_integrations/phoenix_app
run: |
rm -f tmp/sentry_debug_events.log
SENTRY_E2E_TEST_MODE=true mix phx.server &
echo $! > /tmp/phoenix.pid
echo "Phoenix server started with PID $(cat /tmp/phoenix.pid)"

- name: Start Svelte server
working-directory: test_integrations/tracing/svelte_mini
run: |
SENTRY_E2E_SVELTE_APP_PORT=4001 npm run dev &
echo $! > /tmp/svelte.pid
echo "Svelte server started with PID $(cat /tmp/svelte.pid)"

- name: Wait for Phoenix server
run: |
echo "Waiting for Phoenix server at http://localhost:4000/health..."
timeout 60 bash -c 'until curl -s http://localhost:4000/health > /dev/null 2>&1; do echo "Waiting..."; sleep 2; done'
echo "Phoenix server is ready!"
curl -s http://localhost:4000/health

- name: Wait for Svelte server
run: |
echo "Waiting for Svelte server at http://localhost:4001/health..."
timeout 60 bash -c 'until curl -s http://localhost:4001/health > /dev/null 2>&1; do echo "Waiting..."; sleep 2; done'
echo "Svelte server is ready!"
curl -s http://localhost:4001/health

- name: Run Playwright tests
working-directory: test_integrations/tracing
env:
SENTRY_E2E_PHOENIX_APP_URL: http://localhost:4000
SENTRY_E2E_SVELTE_APP_URL: http://localhost:4001
SENTRY_E2E_SERVERS_RUNNING: "true"
run: npx playwright test --reporter=list

- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: test_integrations/tracing/playwright-report/
retention-days: 7

- name: Upload test screenshots
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-screenshots
path: test_integrations/tracing/test-results/
retention-days: 7

- name: Show Phoenix server logs
if: failure()
run: |
echo "=== Sentry debug events log ==="
cat test_integrations/phoenix_app/tmp/sentry_debug_events.log 2>/dev/null || echo "No events logged"

- name: Cleanup servers
if: always()
run: |
[ -f /tmp/phoenix.pid ] && kill $(cat /tmp/phoenix.pid) 2>/dev/null || true
[ -f /tmp/svelte.pid ] && kill $(cat /tmp/svelte.pid) 2>/dev/null || true
4 changes: 3 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
uses: actions/checkout@v4

- name: Setup Elixir and Erlang
uses: erlef/setup-beam@v1
uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9
with:
elixir-version: ${{ matrix.elixir }}
otp-version: ${{ matrix.otp }}
Expand Down Expand Up @@ -117,6 +117,8 @@ jobs:
run: mix test

- name: Run integration tests
env:
SKIP_TRACING_E2E: "true"
run: mix test.integrations

- name: Cache Dialyzer PLT
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ test_integrations/phoenix_app/db

test_integrations/*/_build
test_integrations/*/deps
test_integrations/*/test-results/
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

#### Features

- Support for Distributed Tracing ([957](https://github.com/getsentry/sentry-elixir/pull/957))

## 11.0.4

- Fix safe JSON encoding of improper lists ([#938](https://github.com/getsentry/sentry-elixir/pull/938))
Expand Down
135 changes: 135 additions & 0 deletions lib/sentry/opentelemetry/propagator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
defmodule Sentry.OpenTelemetry.Propagator do
@moduledoc """
OpenTelemetry propagator for Sentry distributed tracing.

This propagator implements the `sentry-trace` and `sentry-baggage` header propagation
to enable distributed tracing across service boundaries. It follows the W3C Trace Context.
"""

import Bitwise

require Record
require OpenTelemetry.Tracer, as: Tracer

@behaviour :otel_propagator_text_map

@fields Record.extract(:span_ctx, from_lib: "opentelemetry_api/include/opentelemetry.hrl")
Record.defrecordp(:span_ctx, @fields)

@sentry_trace_key "sentry-trace"
@sentry_baggage_key "baggage"
@sentry_trace_ctx_key :"sentry-trace"
@sentry_baggage_ctx_key :"sentry-baggage"

@impl true
def fields(_opts) do
[@sentry_trace_key, @sentry_baggage_key]
end

@impl true
def inject(ctx, carrier, setter, _opts) do
case Tracer.current_span_ctx(ctx) do
span_ctx(trace_id: tid, span_id: sid, trace_flags: flags) when tid != 0 and sid != 0 ->
sentry_trace_header = encode_sentry_trace({tid, sid, flags})
carrier = setter.(@sentry_trace_key, sentry_trace_header, carrier)

baggage_value = :otel_ctx.get_value(ctx, @sentry_baggage_ctx_key, :not_found)

if is_binary(baggage_value) and baggage_value != :not_found do
setter.(@sentry_baggage_key, baggage_value, carrier)
else
carrier
end

_ ->
carrier
end
end

@impl true
def extract(ctx, carrier, _keys_fun, getter, _opts) do
case getter.(@sentry_trace_key, carrier) do
:undefined ->
ctx

header when is_binary(header) ->
case decode_sentry_trace(header) do
{:ok, {trace_hex, span_hex, sampled}} ->
ctx =
ctx
|> :otel_ctx.set_value(@sentry_trace_ctx_key, {trace_hex, span_hex, sampled})
|> maybe_set_baggage(getter.(@sentry_baggage_key, carrier))

trace_id = hex_to_int(trace_hex)
span_id = hex_to_int(span_hex)

# Create a remote, sampled parent span in the OTEL context.
# We will set to "always sample" because Sentry will decide real sampling
remote_span_ctx = :otel_tracer.from_remote_span(trace_id, span_id, 1)

Tracer.set_current_span(ctx, remote_span_ctx)

{:error, _reason} ->
ctx
end

_ ->
ctx
end
end

# Encode trace ID, span ID, and sampled flag to sentry-trace header format
# Format: {trace_id}-{span_id}-{sampled}
defp encode_sentry_trace({trace_id_int, span_id_int, trace_flags}) do
sampled = if (trace_flags &&& 1) == 1, do: "1", else: "0"
int_to_hex(trace_id_int, 16) <> "-" <> int_to_hex(span_id_int, 8) <> "-" <> sampled
end

# Decode sentry-trace header
# Format: {trace_id}-{span_id}-{sampled} or {trace_id}-{span_id}
defp decode_sentry_trace(
<<trace_hex::binary-size(32), "-", span_hex::binary-size(16), "-",
sampled::binary-size(1)>>
) do
{:ok, {trace_hex, span_hex, sampled == "1"}}
end

defp decode_sentry_trace(<<trace_hex::binary-size(32), "-", span_hex::binary-size(16)>>) do
{:ok, {trace_hex, span_hex, false}}
end

defp decode_sentry_trace(_invalid) do
{:error, :invalid_format}
end

defp maybe_set_baggage(ctx, :undefined), do: ctx
defp maybe_set_baggage(ctx, ""), do: ctx
defp maybe_set_baggage(ctx, nil), do: ctx

defp maybe_set_baggage(ctx, baggage) when is_binary(baggage) do
:otel_ctx.set_value(ctx, @sentry_baggage_ctx_key, baggage)
end

# Convert hex string to integer
defp hex_to_int(hex) do
hex
|> Base.decode16!(case: :mixed)
|> :binary.decode_unsigned()
end

# Convert integer to hex string with padding
defp int_to_hex(value, num_bytes) do
value
|> :binary.encode_unsigned()
|> bin_pad_left(num_bytes)
|> Base.encode16(case: :lower)
end

# Pad binary to specified number of bytes
defp bin_pad_left(bin, total_bytes) do
missing = total_bytes - byte_size(bin)
if missing > 0, do: :binary.copy(<<0>>, missing) <> bin, else: bin
end
end
end
Loading