rspec-mergify 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f6831a7105994678c61d541f3ca3840a618f93ed59561cecd23063d91063d7e
4
- data.tar.gz: e6b1528a39efa7de31ef4ded5d44b2479f78521666cf48acb26edc6135580067
3
+ metadata.gz: 2d222991a085c33d95515483a7a41823b3db8c978d71e9e34a5c2ba8d4ca1777
4
+ data.tar.gz: a555d7a13c32f2bc1205c2060b5bd353b06f2b72c3460119d2800549f68a1c89
5
5
  SHA512:
6
- metadata.gz: acc68c52030c7cf29760e293f99a46faae5c012745f0cd6d9b81c36cbb5c29505b7d16662a1785fd641f3f4ba8e424b592782308ef06356c3694e9bbfe09bddc
7
- data.tar.gz: c95fc1c14925f61b63d1888e51e47f88af3ced37565a4964e6c30b913f0cbc6261a212562578f41faef937c1a0665fc0a150b302fd83e270bc991bcefb76b663
6
+ metadata.gz: 3908a8fb75af4875c54edfd828e1d638ca85730597d8283c8a558a06e50023b62345881a6bfbbcd37f1c69aef4e0d6043feb8bb027f00e83b492752295b2a8e2
7
+ data.tar.gz: ea86ab73dbab04394f1cd4c5718a4347592adf940aa1b82c2c54ccc4a56fa540a120127c54eb244531cfd90a94c38907bf22ec4f7c49129defeb57d5594fe2ac
data/README.md CHANGED
@@ -7,6 +7,7 @@ RSpec plugin for [Mergify Test Insights](https://docs.mergify.com/ci-insights/).
7
7
  - **Test tracing** — Sends OpenTelemetry traces for every test to Mergify's API
8
8
  - **Flaky test detection** — Intelligently reruns tests to detect flakiness with budget constraints
9
9
  - **Test quarantine** — Quarantines failing tests so they don't block CI
10
+ - **Test selection** — Runs only the previously-failing examples when Mergify's merge queue reruns a job
10
11
 
11
12
  ## Installation
12
13
 
@@ -18,6 +19,23 @@ gem 'rspec-mergify'
18
19
 
19
20
  Then run `bundle install`.
20
21
 
22
+ ### Supported platforms
23
+
24
+ The gem ships precompiled, so nothing is built on your machine. Each platform gem carries an extension for Ruby 3.1 through 4.0:
25
+
26
+ | Platform | Requirement |
27
+ |---|---|
28
+ | Linux x86_64 / aarch64 (glibc) | **glibc 2.30 or newer** — Debian 11+, Ubuntu 20.04+, RHEL 9+, Amazon Linux 2023 |
29
+ | Linux x86_64 / aarch64 (musl) | Alpine; on Ruby 3.1 also `apk add libgcc` |
30
+ | macOS arm64 / x86_64 | — |
31
+ | Windows x64 (UCRT) | — |
32
+
33
+ Anywhere else, and on glibc older than 2.30 — RHEL, Rocky, AlmaLinux and Oracle Linux 8, Amazon Linux 2, CentOS 7 — the extension cannot load. The gem still installs and your suite still runs, but nothing is reported to Mergify. Pin the last pure-Ruby release on those systems:
34
+
35
+ ```ruby
36
+ gem 'rspec-mergify', '0.1.4'
37
+ ```
38
+
21
39
  ## Configuration
22
40
 
23
41
  Set the `MERGIFY_TOKEN` environment variable with your Mergify API token.
@@ -34,6 +52,54 @@ The plugin activates automatically when running in CI (detected via the `CI` env
34
52
  | `RSPEC_MERGIFY_DEBUG` | Print spans to console | `false` |
35
53
  | `MERGIFY_TRACEPARENT` | W3C distributed trace context | — |
36
54
  | `MERGIFY_TEST_JOB_NAME` | Mergify test job name | — |
55
+ | `MERGIFY_TEST_SELECTION_ENABLE` | Opt this job into test selection (see below) | `false` |
56
+
57
+ ### Test selection
58
+
59
+ When Mergify's merge queue reruns a job — a retry, or a step of a batch
60
+ bisection — only the examples that failed on the previous attempt are
61
+ informative. The gem asks Mergify whether the current run is such a rerun and,
62
+ if so, runs only those examples.
63
+
64
+ **This is off until you turn it on, per job.** Set
65
+ `MERGIFY_TEST_SELECTION_ENABLE=true` on the job you want reduced. Installing the
66
+ gem is not enough — a feature that decides not to run tests starts only where
67
+ you wrote that it should. Anything that is not a recognised yes (unset, empty,
68
+ `false`, unparsable) means no, and a job that has not opted in never asks.
69
+
70
+ Once opted in, it only ever removes work:
71
+
72
+ - the answer is applied **after** RSpec's own filters — file and line
73
+ arguments, `--tag`, `-e`, `--only-failures` all still narrow the run, never
74
+ widen it;
75
+ - examples are matched by their id (`./spec/models/user_spec.rb[1:2]`), the
76
+ identity RSpec itself reruns failures by;
77
+ - if Mergify says the previous attempt already ran every one of these examples
78
+ and they passed, the run executes none and exits green;
79
+ - if any example Mergify asks for is not collected here, the full suite runs;
80
+ - any error, timeout, or unrecognised answer runs the full suite;
81
+ - if Mergify refuses to pick a previous attempt (several runs of this job
82
+ report under one name), the run **fails** with Mergify's explanation: give
83
+ each run its own `MERGIFY_TEST_JOB_NAME`;
84
+ - at the end of the run the gem sends Mergify what it concluded (the counts
85
+ and the failing examples) — unless the run failed outside any example (a
86
+ failing `after(:context)` or suite hook), in which case a retry runs the
87
+ full suite.
88
+
89
+ Each parallel_tests worker, and each CI job handed a slice of the spec files,
90
+ is matched to the same slice of the previous attempt, by the examples it
91
+ collects. This needs the slices to be the same on both attempts, which is the
92
+ case for a split by file size or by a committed runtime log; Knapsack Pro's
93
+ Queue Mode hands files out dynamically and is not supported.
94
+
95
+ ### Parallel runs
96
+
97
+ [parallel_tests](https://github.com/grosser/parallel_tests) and [turbo_tests](https://github.com/serpapi/turbo_tests) need no setup. Each worker is a separate RSpec process, and each reports to Mergify as its own session:
98
+
99
+ - flaky detection sizes each worker's rerun budget from the tests that worker runs;
100
+ - each worker prints its own Mergify report, so a report covers only that worker's tests.
101
+
102
+ Suites split across CI jobs (knapsack, `circleci tests split`, Buildkite `parallelism`) behave the same way, one session per job.
37
103
 
38
104
  For detailed documentation, see the [official guide](https://docs.mergify.com/ci-insights/test-frameworks/rspec/).
39
105
 
@@ -1,40 +1,172 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
- require 'opentelemetry-sdk'
4
+ require_relative 'trace'
5
5
  require_relative 'utils'
6
6
  require_relative 'native'
7
- require_relative 'synchronous_batch_span_processor'
8
7
  require_relative 'resources/rspec'
8
+ require_relative 'test_selection'
9
+ require_relative 'session_verdict'
9
10
 
10
11
  module Mergify
11
12
  module RSpec
12
- # Central orchestrator for Mergify Test Insights: sets up OpenTelemetry tracing,
13
- # manages the tracer provider, and coordinates flaky detection and quarantine.
13
+ # Central orchestrator for Mergify Test Insights: records the run's spans,
14
+ # uploads them, and coordinates flaky detection, quarantine and test
15
+ # selection.
14
16
  # rubocop:disable-next Metrics/ClassLength
15
17
  class CIInsights
18
+ # The resource attributes pytest-mergify reports, under the same keys.
19
+ # The fingerprint and count describe what this run collected, on every
20
+ # run that took one; the selection keys echo what Mergify answered, as
21
+ # sent, and are absent on a run it never answered.
22
+ TEST_COLLECTION_FINGERPRINT = 'test.collection.fingerprint'
23
+ TEST_COLLECTION_COUNT = 'test.collection.count'
24
+ TEST_SELECTION_ANSWER = 'test.selection.answer'
25
+ TEST_SELECTION_REASON = 'test.selection.reason'
26
+ TEST_SELECTION_KEPT_COUNT = 'test.selection.kept_count'
27
+ TEST_SELECTION_NOT_APPLIED_REASON = 'test.selection.not_applied_reason'
28
+
29
+ # The largest value the engine's counters take (a signed 32-bit int).
30
+ MAX_COUNT = (2**31) - 1
31
+
16
32
  attr_reader :token, :repo_name, :api_url, :test_run_id,
17
- :tracer_provider, :tracer, :exporter,
33
+ :recorder,
18
34
  :branch_name,
19
- :flaky_detector, :flaky_detector_error_message, :quarantined_tests
35
+ :flaky_detector, :flaky_detector_error_message, :quarantined_tests,
36
+ :test_selection, :session_verdict, :session_verdict_result,
37
+ :captured_session_verdict, :session_verdict_withheld
20
38
 
21
- # rubocop:disable-next Metrics/MethodLength
39
+ # rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize
22
40
  def initialize
23
41
  @token = ENV.fetch('MERGIFY_TOKEN', nil)
24
42
  @repo_name = Native.detect_repository_name
25
43
  @api_url = ENV.fetch('MERGIFY_API_URL', 'https://api.mergify.com')
26
44
  @test_run_id = SecureRandom.hex(8)
27
- @tracer_provider = nil
28
- @tracer = nil
29
- @exporter = nil
45
+ @recorder = nil
46
+ @uploads = false
30
47
  @branch_name = nil
31
48
  @flaky_detector = nil
32
49
  @flaky_detector_error_message = nil
33
50
  @quarantined_tests = nil
51
+ @test_selection = nil
52
+ @selection_echo = nil
53
+ @session_verdict = SessionVerdict.new
54
+ @session_verdict_result = SessionVerdict::Result.new(sent: false, truncated: false)
55
+ @captured_session_verdict = nil
56
+ @session_verdict_withheld = false
34
57
 
35
58
  setup_tracing if Utils.in_ci?
36
59
  end
37
60
 
61
+ # Take the identity of what this run is about to execute, and ask
62
+ # Mergify whether part of it is enough.
63
+ #
64
+ # Called once RSpec has loaded the spec files and applied every filter
65
+ # of its own -- file and line arguments, `--tag`, `-e`, `--only-failures`
66
+ # -- so the fingerprint describes this process's examples and nothing
67
+ # else. A parallel_tests worker, or a CI job handed a slice of the spec
68
+ # files, collects its own slice and so reports its own identity: the
69
+ # engine matches each against the previous attempt of the same slice.
70
+ #
71
+ # The fingerprint is reported on every run that can take one, whether
72
+ # or not this job opted into the selection, as pytest-mergify does.
73
+ def on_examples_collected(example_ids)
74
+ return unless @recorder && Native.available?
75
+
76
+ fingerprint = Native.test_collection_fingerprint(example_ids)
77
+ @recorder.resource_attributes[TEST_COLLECTION_FINGERPRINT] = fingerprint
78
+ @recorder.resource_attributes[TEST_COLLECTION_COUNT] = example_ids.size
79
+ # A process with nothing to run has nothing to reduce, and asking would
80
+ # cost the job its next retry: every parallel worker left empty by a
81
+ # filter reports the same empty-set fingerprint under the same job, and
82
+ # Mergify refuses to choose between sessions it cannot tell apart.
83
+ load_test_selection(fingerprint) unless example_ids.empty?
84
+ end
85
+
86
+ # Report what Mergify answered and what this run made of it, once the
87
+ # answer has met the collection. Nothing is echoed unless Mergify
88
+ # actually answered: a run that never asked, or whose question went
89
+ # unanswered, was offered nothing.
90
+ # rubocop:disable-next Metrics/MethodLength
91
+ def on_selection_resolved(kept_count)
92
+ return unless @recorder && @test_selection&.served?
93
+
94
+ @selection_echo = {
95
+ 'answer' => @test_selection.selection,
96
+ 'reason' => @test_selection.reason,
97
+ 'kept_count' => kept_count
98
+ }
99
+ not_applied = @test_selection.not_applied_reason
100
+ @selection_echo['not_applied_reason'] = not_applied if not_applied
101
+
102
+ resource = @recorder.resource_attributes
103
+ resource[TEST_SELECTION_ANSWER] = @selection_echo['answer']
104
+ resource[TEST_SELECTION_REASON] = @selection_echo['reason']
105
+ resource[TEST_SELECTION_KEPT_COUNT] = kept_count
106
+ resource[TEST_SELECTION_NOT_APPLIED_REASON] = not_applied if not_applied
107
+ end
108
+
109
+ # Write what this session concluded to Mergify, before the spans go.
110
+ #
111
+ # Sent exactly when the selection was asked for -- including when that
112
+ # request failed: the verdict is what the NEXT rerun of this job needs.
113
+ # Withheld when the run failed outside any example (a failing
114
+ # `after(:context)` or suite hook): its examples then read as passed,
115
+ # and a verdict naming no failure would have the retry run nothing and
116
+ # turn green on the same breakage. Without a verdict, the retry runs the
117
+ # whole suite. Never raises.
118
+ # rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
119
+ def send_session_verdict(failed_outside_examples:)
120
+ return if @test_selection.nil?
121
+
122
+ if failed_outside_examples
123
+ @session_verdict_withheld = true
124
+ return
125
+ end
126
+
127
+ body = session_verdict_body
128
+ return if body.nil?
129
+
130
+ if test_mode?
131
+ @captured_session_verdict = body
132
+ @session_verdict_result = SessionVerdict::Result.new(sent: true, truncated: false)
133
+ return
134
+ end
135
+
136
+ if debug_mode?
137
+ puts "MERGIFY SESSION VERDICT: #{body}"
138
+ @session_verdict_result = SessionVerdict::Result.new(sent: true, truncated: false)
139
+ return
140
+ end
141
+
142
+ receipt = api_client.send_session_verdict(body)
143
+ # Dormant: the feature is not enabled for this repository, which the
144
+ # selection block already says.
145
+ return if receipt.nil?
146
+
147
+ @session_verdict_result = SessionVerdict::Result.new(sent: true, truncated: receipt['truncated'])
148
+ rescue StandardError => e
149
+ # Broader than ApiError on purpose: a value the binding cannot marshal
150
+ # raises something else, and a verdict must never cost the run.
151
+ message = e.is_a?(Native::ApiError) ? e.message : "#{e.class}: #{e.message}"
152
+ @session_verdict_result = SessionVerdict::Result.new(sent: false, truncated: false, error: message)
153
+ end
154
+
155
+ # Send the run, once, at the end. Failing to report a run is worth
156
+ # saying out loud but never worth failing a suite that just passed,
157
+ # so this answers with a message instead of raising.
158
+ def flush
159
+ return nil unless @recorder && @uploads
160
+ return nil if @recorder.finished_spans.empty?
161
+
162
+ owner, repo = Utils.split_full_repo_name(@repo_name)
163
+ client = Native::Client.new(@api_url, @token, owner, repo, Mergify::RSpec::VERSION)
164
+ client.upload_trace(@recorder.resource_attributes, @recorder.finished_spans.map(&:to_h))
165
+ nil
166
+ rescue Native::ApiError, Utils::InvalidRepositoryFullNameError => e
167
+ e.message
168
+ end
169
+
38
170
  def mark_test_as_quarantined_if_needed(example_id) # rubocop:disable Naming/PredicateMethod
39
171
  return false unless @quarantined_tests&.include?(example_id)
40
172
 
@@ -44,30 +176,104 @@ module Mergify
44
176
 
45
177
  private
46
178
 
47
- def setup_tracing
48
- processor, exp = build_processor
49
- return unless processor
179
+ # Opt-in, per job, and asked for only where the run's identity is
180
+ # complete: the head branch and revision (a merge-queue draft branch on a
181
+ # rerun) and the job coordinates, the very values each uploaded example
182
+ # carries, so the server can match its records.
183
+ # rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity
184
+ def load_test_selection(fingerprint)
185
+ return unless TestSelection.enabled?
186
+ # An empty token is what a fork or a bot pull request gets for a secret.
187
+ return if @token.to_s.empty? || @repo_name.to_s.empty?
50
188
 
51
- @exporter = exp
52
- resource = build_resource
53
- @tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new(resource: resource)
54
- @tracer_provider.add_span_processor(processor)
55
- @tracer = @tracer_provider.tracer('rspec-mergify', Mergify::RSpec::VERSION)
56
- @branch_name = extract_branch_name(resource)
57
- load_flaky_detector
58
- load_quarantine
189
+ resource = @recorder.resource_attributes
190
+ branch = resource['vcs.ref.head.name']
191
+ head_sha = resource['vcs.ref.head.revision']
192
+ pipeline_name = resource['cicd.pipeline.name']
193
+ job_name = job_name_from(resource)
194
+ return if [branch, head_sha, pipeline_name, job_name].any? { |value| value.to_s.empty? }
195
+
196
+ answer = api_client.fetch_test_selection(branch.to_s, head_sha.to_s, pipeline_name.to_s,
197
+ job_name.to_s, fingerprint)
198
+ @test_selection = answer.nil? ? TestSelection.unanswered : TestSelection.served(answer)
199
+ rescue Native::ApiError, Utils::InvalidRepositoryFullNameError => e
200
+ @test_selection = TestSelection.unanswered(init_error_msg: e.message)
201
+ end
202
+
203
+ # `mergify.test.job.name` is the operator-set override; the provider's
204
+ # own task name is the fallback. Same precedence as the other clients.
205
+ def job_name_from(resource)
206
+ override = resource['mergify.test.job.name']
207
+ override.to_s.empty? ? resource['cicd.pipeline.task.name'] : override
208
+ end
209
+
210
+ # The verdict body, keyed on exactly what the selection call was keyed
211
+ # on, so a verdict is found by what the asking run knows.
212
+ # rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize
213
+ def session_verdict_body
214
+ resource = @recorder.resource_attributes
215
+ fingerprint = resource[TEST_COLLECTION_FINGERPRINT]
216
+ head_sha = resource['vcs.ref.head.revision']
217
+ pipeline_name = resource['cicd.pipeline.name']
218
+ job_name = job_name_from(resource)
219
+ return nil if [fingerprint, head_sha, pipeline_name, job_name].any? { |value| value.to_s.empty? }
220
+
221
+ body = {
222
+ 'test_run_id' => @test_run_id,
223
+ 'head_sha' => head_sha.to_s,
224
+ 'pipeline_name' => pipeline_name.to_s,
225
+ 'job_name' => job_name.to_s,
226
+ 'collection_fingerprint' => fingerprint,
227
+ 'collection_count' => resource[TEST_COLLECTION_COUNT],
228
+ 'total_test_runtime_ms' => @session_verdict.total_test_runtime_ms,
229
+ 'failing_tests' => @session_verdict.failing_tests,
230
+ 'quarantined_failing_tests' => @session_verdict.quarantined_failing_tests
231
+ }.merge(@session_verdict.counts)
232
+ head_branch = resource['vcs.ref.head.name']
233
+ body['head_branch'] = head_branch.to_s unless head_branch.to_s.empty?
234
+ add_run_identity(body, resource)
235
+ body['selection'] = @selection_echo.dup if @selection_echo
236
+ body
59
237
  end
60
238
 
61
- def build_processor
62
- if debug_mode? || test_mode?
63
- build_in_memory_processor
64
- elsif @token && @repo_name
65
- build_otlp_processor
66
- else
67
- [nil, nil]
239
+ # `run_attempt` only next to a `run_id`: the engine refuses an attempt of
240
+ # nothing. An attempt the engine's counter cannot hold is left out
241
+ # rather than refused.
242
+ def add_run_identity(body, resource)
243
+ run_id = resource['cicd.pipeline.run.id']
244
+ return if run_id.to_s.empty?
245
+
246
+ body['run_id'] = run_id.to_s
247
+ attempt = resource['cicd.pipeline.run.attempt']
248
+ body['run_attempt'] = attempt if attempt.is_a?(Integer) && attempt.between?(0, MAX_COUNT)
249
+ end
250
+
251
+ def api_client
252
+ @api_client ||= begin
253
+ owner, repo = Utils.split_full_repo_name(@repo_name)
254
+ Native::Client.new(@api_url, @token, owner, repo, Mergify::RSpec::VERSION)
68
255
  end
69
256
  end
70
257
 
258
+ # Recording is unconditional; whether the run is *uploaded* is what the
259
+ # token and repository decide. Debug and test runs keep their spans and
260
+ # send nothing, which is what they always did -- the difference is that
261
+ # the collector is the same object either way instead of two processors.
262
+ def setup_tracing
263
+ resource = build_resource
264
+ @recorder = Trace::Recorder.new(resource_attributes: resource,
265
+ traceparent: ENV.fetch('MERGIFY_TRACEPARENT', nil))
266
+ @uploads = uploadable?
267
+ # Only a pull request has a base branch, and that is what puts flaky
268
+ # detection in 'new' mode. GitHub Actions still sets GITHUB_BASE_REF on
269
+ # every other event, to an empty string, so empty counts as absent.
270
+ base_branch_name = resource['vcs.ref.base.name']
271
+ @base_branch_name = base_branch_name unless base_branch_name.to_s.empty?
272
+ @branch_name = @base_branch_name || resource['vcs.ref.head.name']
273
+ load_flaky_detector
274
+ load_quarantine
275
+ end
276
+
71
277
  def debug_mode?
72
278
  ENV.key?('RSPEC_MERGIFY_DEBUG')
73
279
  end
@@ -76,59 +282,23 @@ module Mergify
76
282
  ENV['_RSPEC_MERGIFY_TEST'] == 'true'
77
283
  end
78
284
 
79
- def build_in_memory_processor
80
- exp = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new
81
- processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exp)
82
- [processor, exp]
83
- end
285
+ # A run is uploaded when there is somewhere to upload it to and nothing
286
+ # asking us not to: debug and test runs record and keep.
287
+ def uploadable?
288
+ return false if debug_mode? || test_mode?
289
+ return false unless @token && @repo_name && Native.available?
84
290
 
85
- def build_otlp_processor
86
- owner, repo = Utils.split_full_repo_name(@repo_name)
87
- endpoint = "#{@api_url}/v1/ci/#{owner}/repositories/#{repo}/traces"
88
- exp = create_otlp_exporter(endpoint)
89
- processor = SynchronousBatchSpanProcessor.new(exp)
90
- [processor, exp]
291
+ true
91
292
  end
92
293
 
93
294
  # The cicd.* and vcs.* attributes come from the Rust core, which every
94
- # Mergify test client shares, so a provider gains them everywhere at once.
95
- # What stays here is what only Ruby knows: the test framework, and the id
96
- # this run invented for itself.
295
+ # Mergify test client shares, so a CI provider added there reaches every
296
+ # client at once. What stays here is what only Ruby knows: the test
297
+ # framework and its language, and the id this run invented for itself.
97
298
  def build_resource
98
- resources = [
99
- OpenTelemetry::SDK::Resources::Resource.create(Native.detect_attributes),
100
- Resources::RSpec.detect,
101
- OpenTelemetry::SDK::Resources::Resource.create('test.run.id' => @test_run_id)
102
- ]
103
- resources.reduce(OpenTelemetry::SDK::Resources::Resource.create({})) do |merged, r|
104
- merged.merge(r)
105
- end
106
- end
107
-
108
- def extract_branch_name(resource)
109
- attrs = resource.attribute_enumerator.to_h
110
- @base_branch_name = attrs['vcs.ref.base.name']
111
- @base_branch_name || attrs['vcs.ref.head.name']
112
- end
113
-
114
- # rubocop:disable-next Metrics/MethodLength
115
- def create_otlp_exporter(endpoint)
116
- require 'opentelemetry-exporter-otlp'
117
- original_env = ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', nil)
118
- ENV['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'] = endpoint
119
- begin
120
- OpenTelemetry::Exporter::OTLP::Exporter.new(
121
- endpoint: endpoint,
122
- headers: { 'Authorization' => "Bearer #{@token}" },
123
- compression: 'gzip'
124
- )
125
- ensure
126
- if original_env
127
- ENV['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'] = original_env
128
- else
129
- ENV.delete('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT')
130
- end
131
- end
299
+ Native.detect_attributes
300
+ .merge(Resources::RSpec.detect)
301
+ .merge('test.run.id' => @test_run_id)
132
302
  end
133
303
 
134
304
  # rubocop:disable-next Metrics/MethodLength