Skip to content

Commit eeed499

Browse files
fix(error_reporting): respect quota_project configuration (#34610)
This PR fixes an issue where the `Google::Cloud::ErrorReporting` client was completely ignoring the `quota_project` configuration parameter, causing API calls to default billing to the project associated with the credentials. This resulted in `PermissionDeniedError` (Error Reporting API not enabled) for users attempting to report errors to a target project that was different from their credentials project (e.g., issue #25862). ### Changes * Respect Configuration: Updated `Google::Cloud::ErrorReporting.new` to correctly resolve `quota_project` from the library configuration and pipe it down to the internal `Service` layer. * Service Wrapper: Updated `Google::Cloud::ErrorReporting::Service` to accept `quota_project` (falling back to credentials) and apply it to the underlying gRPC client configuration. * Documentation: Added multi-project configuration examples and descriptions to `README.md` and `AUTHENTICATION.md`. * Testing: Added unit test coverage to confirm configuration propagation and refined YARD doctest mock expectations. * Modernization: Modernized `Google::Cloud::ErrorReporting::Project#report` to use Ruby 3.2+ anonymous block forwarding (`*args, &`), resolving pre-existing RuboCop style offenses. * Monorepo Style: Resolved `Lint/SafeNavigationWithEmpty` in top-level `.toys/ci.rb` to ensure modern Ruby CI workflows pass cleanly. TAG=agy CONV=8c6d3acb-f6a5-49ce-8263-7c87d2430e6f Co-authored-by: Yoshi Automation Bot <yoshi-automation@google.com>
1 parent c923a25 commit eeed499

9 files changed

Lines changed: 133 additions & 20 deletions

File tree

.toys/ci.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,8 @@ def interpret_github_event
237237
logger.info "Using local commits"
238238
[base_commit, head_commit]
239239
end
240-
base_ref = nil if base_ref&.empty?
241-
head_ref = nil if head_ref&.empty?
240+
base_ref = nil if base_ref && base_ref.empty?
241+
head_ref = nil if head_ref && head_ref.empty?
242242
[base_ref, head_ref]
243243
end
244244

google-cloud-error_reporting/AUTHENTICATION.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,20 +96,24 @@ client = Google::Cloud::ErrorReporting.new
9696
9797
### Configuration
9898
99-
The **Project ID** and the path to the **Credentials JSON** file can be configured
99+
The **Project ID**, **Quota Project**, and the path to the **Credentials JSON** file can be configured
100100
instead of placing them in environment variables or providing them as arguments.
101101
102102
```ruby
103103
require "google/cloud/error_reporting"
104104
105105
Google::Cloud::ErrorReporting.configure do |config|
106-
config.project_id = "my-project-id"
107-
config.credentials = "path/to/keyfile.json"
106+
config.project_id = "my-project-id" # The project where errors are reported
107+
config.quota_project = "my-billing-project" # The project billed for quota/billing (optional)
108+
config.credentials = "path/to/keyfile.json"
108109
end
109110
110111
client = Google::Cloud::ErrorReporting.new
111112
```
112113
114+
> [!NOTE]
115+
> **Project ID** (where errors are sent) and **Quota Project** (which project is billed for the API call) are distinct. By default, the library bills the project associated with the credentials. Use `config.quota_project` if you need to bill a different project.
116+
113117
### Cloud SDK
114118
115119
This option allows for an easy way to authenticate during development. If

google-cloud-error_reporting/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,12 @@ Google::Cloud.configure do |config|
168168
# Shared parameters
169169
config.project_id = "your-project-id"
170170
config.keyfile = "/path/to/key.json"
171+
config.quota_project = "your-billing-project"
172+
171173
# Or Error Reporting specific parameters
172174
config.error_reporting.project_id = "your-project-id"
173175
config.error_reporting.keyfile = "/path/to/key.json"
176+
config.error_reporting.quota_project = "your-billing-project"
174177
end
175178
```
176179

google-cloud-error_reporting/lib/google/cloud/error_reporting.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ def self.new project_id: nil,
9393
credentials = resolve_credentials credentials, scope
9494
project_id = resolve_project_id project_id, credentials
9595

96-
service = ErrorReporting::Service.new project_id, credentials, host: endpoint, timeout: timeout
96+
quota_project = configure.quota_project
97+
service = ErrorReporting::Service.new project_id, credentials,
98+
host: endpoint, timeout: timeout,
99+
quota_project: quota_project
97100
ErrorReporting::Project.new service
98101
end
99102

google-cloud-error_reporting/lib/google/cloud/error_reporting/project.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ def project_id
122122
# error_event = error_reporting.error_event "Error with Backtrace"
123123
# error_reporting.report error_event
124124
#
125-
def report *args, &block
126-
service.report(*args, &block)
125+
def report(*args, &)
126+
service.report(*args, &)
127127
end
128128

129129
##

google-cloud-error_reporting/lib/google/cloud/error_reporting/service.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,17 @@ class Service
2929
attr_accessor :credentials
3030
attr_accessor :timeout
3131
attr_accessor :host
32+
attr_accessor :quota_project
3233

3334
##
3435
# Creates a new Service instance.
35-
def initialize project, credentials, timeout: nil, host: nil
36+
def initialize project, credentials, timeout: nil, host: nil, quota_project: nil
3637
@project = project
3738
@credentials = credentials
3839
@timeout = timeout
3940
@host = host
41+
@quota_project = quota_project
42+
@quota_project ||= credentials.quota_project_id if credentials.respond_to? :quota_project_id
4043
end
4144

4245
def error_reporting
@@ -46,6 +49,7 @@ def error_reporting
4649
config.credentials = credentials if credentials
4750
config.timeout = timeout if timeout
4851
config.endpoint = host if host
52+
config.quota_project = quota_project if quota_project
4953
config.lib_name = "gccl"
5054
config.lib_version = Google::Cloud::ErrorReporting::VERSION
5155
end

google-cloud-error_reporting/support/doctest_helper.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
require "minitest/focus"
16+
require "ostruct"
1617

1718
require "google/cloud/error_reporting"
1819

@@ -73,13 +74,13 @@ def mock_error_reporting
7374

7475
doctest.before "Google::Cloud#error_reporting" do
7576
mock_error_reporting do |mock|
76-
mock.expect :report_error_event, nil, [Hash]
77+
mock.expect :report_error_event, nil, [], project_name: String, event: Google::Cloud::ErrorReporting::V1beta1::ReportedErrorEvent
7778
end
7879
end
7980

8081
doctest.before "Google::Cloud.error_reporting" do
8182
mock_error_reporting do |mock|
82-
mock.expect :report_error_event, nil, [Hash]
83+
mock.expect :report_error_event, nil, [], project_name: String, event: Google::Cloud::ErrorReporting::V1beta1::ReportedErrorEvent
8384
end
8485
end
8586

@@ -89,21 +90,21 @@ def mock_error_reporting
8990

9091
doctest.before "Google::Cloud::ErrorReporting::ErrorEvent" do
9192
mock_error_reporting do |mock|
92-
mock.expect :report_error_event, nil, [Hash]
93+
mock.expect :report_error_event, nil, [], project_name: String, event: Google::Cloud::ErrorReporting::V1beta1::ReportedErrorEvent
9394
end
9495
end
9596

9697
doctest.before "Google::Cloud::ErrorReporting::Project" do
9798
mock_error_reporting do |mock|
98-
mock.expect :report_error_event, nil, [Hash]
99+
mock.expect :report_error_event, nil, [], project_name: String, event: Google::Cloud::ErrorReporting::V1beta1::ReportedErrorEvent
99100
end
100101
end
101102

102103
doctest.skip "Google::Cloud::ErrorReporting::Credentials" # occasionally getting "This code example is not yet mocked"
103104

104105
doctest.before "Google::Cloud::ErrorReporting::Service" do
105106
mock_error_reporting do |mock|
106-
mock.expect :report_error_event, nil, [Hash]
107+
mock.expect :report_error_event, nil, [], project_name: String, event: Google::Cloud::ErrorReporting::V1beta1::ReportedErrorEvent
107108
end
108109
end
109110
end
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
require "helper"
16+
require "signet/oauth_2/client"
17+
18+
describe Google::Cloud::ErrorReporting::Service do # rubocop:disable Metrics/BlockLength
19+
let(:project) { "test-project" }
20+
let :credentials do
21+
creds = Signet::OAuth2::Client.new
22+
def creds.quota_project_id
23+
"credentials-quota-project"
24+
end
25+
26+
def creds.disable_universe_domain_check
27+
true
28+
end
29+
creds
30+
end
31+
32+
describe ".new" do
33+
it "sets project and credentials" do
34+
service = Google::Cloud::ErrorReporting::Service.new project, credentials
35+
_(service.project).must_equal project
36+
_(service.credentials).must_equal credentials
37+
end
38+
39+
it "accepts quota_project" do
40+
quota_project = "test-quota-project"
41+
service = Google::Cloud::ErrorReporting::Service.new project, credentials, quota_project: quota_project
42+
_(service.quota_project).must_equal quota_project
43+
end
44+
45+
it "falls back to credentials quota_project_id if not explicitly passed" do
46+
service = Google::Cloud::ErrorReporting::Service.new project, credentials
47+
_(service.quota_project).must_equal "credentials-quota-project"
48+
end
49+
end
50+
51+
describe "#error_reporting" do
52+
it "configures the gRPC client with quota_project" do
53+
quota_project = "test-quota-project"
54+
service = Google::Cloud::ErrorReporting::Service.new project, credentials, quota_project: quota_project
55+
56+
client = service.error_reporting
57+
_(client.configure.quota_project).must_equal quota_project
58+
end
59+
60+
it "configures the gRPC client with credentials quota_project" do
61+
service = Google::Cloud::ErrorReporting::Service.new project, credentials
62+
63+
client = service.error_reporting
64+
_(client.configure.quota_project).must_equal "credentials-quota-project"
65+
end
66+
end
67+
end

google-cloud-error_reporting/test/google/cloud/error_reporting_test.rb

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,12 @@ def creds.is_a? target
6666

6767
it "uses provided endpoint" do
6868
endpoint = "errorreporting-endpoint2.example.com"
69-
stubbed_service = ->(project, credentials, timeout: nil, host: nil) {
69+
stubbed_service = ->(project, credentials, timeout: nil, host: nil, quota_project: nil) {
7070
_(project).must_equal "project-id"
7171
_(credentials).must_equal default_credentials
7272
_(timeout).must_be :nil?
7373
_(host).must_equal endpoint
74+
_(quota_project).must_be :nil?
7475
OpenStruct.new project: project
7576
}
7677
ENV.stub :[], nil do
@@ -85,6 +86,7 @@ def creds.is_a? target
8586
end
8687
end
8788

89+
8890
it "uses provided project (alias), keyfile (alias), service, and version" do
8991
stubbed_credentials = ->(keyfile, scope: nil) {
9092
_(keyfile).must_equal "/path/to/a/keyfile"
@@ -315,11 +317,12 @@ def creds.is_a? target
315317
_(scope).must_equal default_scopes
316318
"error_reporting-credentials"
317319
}
318-
stubbed_service = ->(project, credentials, timeout: nil, host: nil) {
320+
stubbed_service = ->(project, credentials, timeout: nil, host: nil, quota_project: nil) {
319321
_(project).must_equal "project-id"
320322
_(credentials).must_equal "error_reporting-credentials"
321323
_(timeout).must_be :nil?
322324
_(host).must_equal default_endpoint
325+
_(quota_project).must_be :nil?
323326
OpenStruct.new project: project
324327
}
325328

@@ -352,11 +355,12 @@ def creds.is_a? target
352355
_(scope).must_equal default_scopes
353356
"error_reporting-credentials"
354357
}
355-
stubbed_service = ->(project, credentials, timeout: nil, host: nil) {
358+
stubbed_service = ->(project, credentials, timeout: nil, host: nil, quota_project: nil) {
356359
_(project).must_equal "project-id"
357360
_(credentials).must_equal "error_reporting-credentials"
358361
_(timeout).must_be :nil?
359362
_(host).must_equal default_endpoint
363+
_(quota_project).must_be :nil?
360364
OpenStruct.new project: project
361365
}
362366

@@ -389,11 +393,12 @@ def creds.is_a? target
389393
_(scope).must_equal default_scopes
390394
"error_reporting-credentials"
391395
}
392-
stubbed_service = ->(project, credentials, timeout: nil, host: nil) {
396+
stubbed_service = ->(project, credentials, timeout: nil, host: nil, quota_project: nil) {
393397
_(project).must_equal "project-id"
394398
_(credentials).must_equal "error_reporting-credentials"
395399
_(timeout).must_equal 42
396400
_(host).must_equal default_endpoint
401+
_(quota_project).must_be :nil?
397402
OpenStruct.new project: project
398403
}
399404

@@ -423,11 +428,12 @@ def creds.is_a? target
423428

424429
it "uses error_reporting config for endpoint" do
425430
endpoint = "errorreporting-endpoint2.example.com"
426-
stubbed_service = ->(project, credentials, timeout: nil, host: nil) {
431+
stubbed_service = ->(project, credentials, timeout: nil, host: nil, quota_project: nil) {
427432
_(project).must_equal "project-id"
428433
_(credentials).must_equal default_credentials
429434
_(timeout).must_be :nil?
430435
_(host).must_equal endpoint
436+
_(quota_project).must_be :nil?
431437
OpenStruct.new project: project
432438
}
433439

@@ -454,11 +460,12 @@ def creds.is_a? target
454460
_(scope).must_equal default_scopes
455461
"error_reporting-credentials"
456462
}
457-
stubbed_service = ->(project, credentials, timeout: nil, host: nil) {
463+
stubbed_service = ->(project, credentials, timeout: nil, host: nil, quota_project: nil) {
458464
_(project).must_equal "project-id"
459465
_(credentials).must_equal "error_reporting-credentials"
460466
_(timeout).must_equal 42
461467
_(host).must_equal default_endpoint
468+
_(quota_project).must_be :nil?
462469
OpenStruct.new project: project
463470
}
464471

@@ -485,5 +492,29 @@ def creds.is_a? target
485492
end
486493
end
487494
end
495+
496+
it "uses error_reporting config for quota_project" do
497+
stubbed_service = lambda do |project, credentials, timeout: nil, host: nil, quota_project: nil|
498+
_(project).must_equal "project-id"
499+
_(credentials).must_equal default_credentials
500+
_(timeout).must_be :nil?
501+
_(host).must_equal default_endpoint
502+
_(quota_project).must_equal "configure-quota-project"
503+
OpenStruct.new project: project
504+
end
505+
506+
ENV.stub :[], nil do
507+
Google::Cloud::ErrorReporting.configure do |config|
508+
config.project_id = "project-id"
509+
config.quota_project = "configure-quota-project"
510+
end
511+
512+
Google::Cloud::ErrorReporting::Service.stub :new, stubbed_service do
513+
error_reporting = Google::Cloud::ErrorReporting.new credentials: default_credentials
514+
_(error_reporting).must_be_kind_of Google::Cloud::ErrorReporting::Project
515+
_(error_reporting.project).must_equal "project-id"
516+
end
517+
end
518+
end
488519
end
489520
end

0 commit comments

Comments
 (0)