analytics_ops 0.1.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 +7 -0
- data/CHANGELOG.md +47 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/CONTRIBUTING.md +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +188 -0
- data/SECURITY.md +56 -0
- data/docs/api-support-matrix.md +50 -0
- data/docs/architecture.md +89 -0
- data/docs/authentication.md +86 -0
- data/docs/commands.md +112 -0
- data/docs/configuration-schema-v1.json +129 -0
- data/docs/configuration.md +167 -0
- data/docs/google-client-compatibility.md +59 -0
- data/docs/plan-format.md +89 -0
- data/docs/plan-schema-v1.json +224 -0
- data/docs/rails.md +73 -0
- data/docs/reports.md +124 -0
- data/docs/safety.md +93 -0
- data/docs/troubleshooting.md +103 -0
- data/exe/analytics-ops +6 -0
- data/lib/analytics_ops/applier.rb +52 -0
- data/lib/analytics_ops/canonical.rb +65 -0
- data/lib/analytics_ops/cli.rb +427 -0
- data/lib/analytics_ops/clients/admin.rb +446 -0
- data/lib/analytics_ops/clients/data.rb +314 -0
- data/lib/analytics_ops/configuration/document.rb +25 -0
- data/lib/analytics_ops/configuration/loader.rb +76 -0
- data/lib/analytics_ops/configuration/schema.rb +107 -0
- data/lib/analytics_ops/configuration/validator.rb +344 -0
- data/lib/analytics_ops/configuration.rb +19 -0
- data/lib/analytics_ops/desired_state.rb +40 -0
- data/lib/analytics_ops/errors.rb +31 -0
- data/lib/analytics_ops/plan.rb +551 -0
- data/lib/analytics_ops/planner.rb +233 -0
- data/lib/analytics_ops/rails/railtie.rb +65 -0
- data/lib/analytics_ops/rails.rb +5 -0
- data/lib/analytics_ops/redaction.rb +46 -0
- data/lib/analytics_ops/reports/catalog.rb +100 -0
- data/lib/analytics_ops/reports/definition.rb +226 -0
- data/lib/analytics_ops/reports/result.rb +90 -0
- data/lib/analytics_ops/reports.rb +13 -0
- data/lib/analytics_ops/resources.rb +79 -0
- data/lib/analytics_ops/snapshot.rb +45 -0
- data/lib/analytics_ops/version.rb +5 -0
- data/lib/analytics_ops/workspace.rb +212 -0
- data/lib/analytics_ops.rb +21 -0
- data/lib/generators/analytics_ops/install_generator.rb +23 -0
- data/lib/generators/analytics_ops/templates/analytics-ops +9 -0
- data/lib/generators/analytics_ops/templates/analytics_ops.yml +23 -0
- data/sig/analytics_ops.rbs +395 -0
- metadata +131 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Strict configuration validation.
|
|
4
|
+
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module AnalyticsOps
|
|
8
|
+
module Configuration
|
|
9
|
+
# Fail-closed validator and normalizer for configuration version 1.
|
|
10
|
+
class Validator
|
|
11
|
+
TOP_LEVEL_KEYS = %w[version profiles].freeze
|
|
12
|
+
PROFILE_KEYS = %w[property_id streams retention google_signals key_events custom_dimensions custom_metrics
|
|
13
|
+
manual_requirements].freeze
|
|
14
|
+
STREAM_KEYS = %w[stream_id default_uri enhanced_measurement].freeze
|
|
15
|
+
ENHANCED_MEASUREMENT_KEYS = %w[enabled experimental].freeze
|
|
16
|
+
RETENTION_KEYS = %w[event_data user_data reset_on_new_activity].freeze
|
|
17
|
+
GOOGLE_SIGNALS_KEYS = %w[state experimental].freeze
|
|
18
|
+
DIMENSION_KEYS = %w[parameter_name display_name description scope disallow_ads_personalization].freeze
|
|
19
|
+
METRIC_KEYS = %w[parameter_name display_name description scope measurement_unit].freeze
|
|
20
|
+
RETENTION_VALUES = %w[2_months 14_months 26_months 38_months 50_months].freeze
|
|
21
|
+
DIMENSION_SCOPES = %w[event user item].freeze
|
|
22
|
+
METRIC_UNITS = %w[standard currency feet meters kilometers miles milliseconds seconds minutes hours].freeze
|
|
23
|
+
SECRET_KEY = /
|
|
24
|
+
(?:\A|_)
|
|
25
|
+
(?:
|
|
26
|
+
credentials?|password|private_key|client_(?:id|secret)|
|
|
27
|
+
access_token|refresh_token|api_key|service_account|oauth
|
|
28
|
+
)
|
|
29
|
+
(?:\z|_)
|
|
30
|
+
/ix
|
|
31
|
+
NAME = /\A[a-z][a-z0-9_]*\z/i
|
|
32
|
+
EVENT_NAME = /\A[a-z][a-z0-9_]{0,39}\z/i
|
|
33
|
+
ID = /\A\d{1,50}\z/
|
|
34
|
+
|
|
35
|
+
def initialize(raw)
|
|
36
|
+
@raw = raw
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call
|
|
40
|
+
hash!(@raw, "$root")
|
|
41
|
+
reject_secret_keys!(@raw)
|
|
42
|
+
exact_keys!(@raw, TOP_LEVEL_KEYS, "$root")
|
|
43
|
+
unless @raw["version"] == 1
|
|
44
|
+
raise UnsupportedVersionError, "Unsupported configuration version #{@raw["version"].inspect}; expected 1"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
profiles = hash!(@raw["profiles"], "profiles")
|
|
48
|
+
raise ConfigurationError, "profiles must contain at least one profile" if profiles.empty?
|
|
49
|
+
|
|
50
|
+
normalized = profiles.to_h do |name, profile|
|
|
51
|
+
validate_name!(name, "profile name")
|
|
52
|
+
[name, validate_profile(name, profile)]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Document.new(version: 1, profiles: normalized)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def validate_profile(name, raw)
|
|
61
|
+
path = "profiles.#{name}"
|
|
62
|
+
profile = hash!(raw, path)
|
|
63
|
+
exact_keys!(profile, PROFILE_KEYS, path)
|
|
64
|
+
property_id = id!(required(profile, "property_id", path), "#{path}.property_id")
|
|
65
|
+
|
|
66
|
+
DesiredState.new(
|
|
67
|
+
profile: name,
|
|
68
|
+
property_id:,
|
|
69
|
+
streams: validate_streams(profile.fetch("streams", {}), path),
|
|
70
|
+
retention: validate_retention(profile["retention"], path),
|
|
71
|
+
key_events: validate_key_events(profile.fetch("key_events", []), path),
|
|
72
|
+
custom_dimensions: validate_dimensions(profile.fetch("custom_dimensions", []), path),
|
|
73
|
+
custom_metrics: validate_metrics(profile.fetch("custom_metrics", []), path),
|
|
74
|
+
manual_requirements: validate_manual_requirements(profile.fetch("manual_requirements", []), path),
|
|
75
|
+
google_signals: validate_google_signals(profile["google_signals"], path)
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def validate_streams(raw, profile_path)
|
|
80
|
+
path = "#{profile_path}.streams"
|
|
81
|
+
streams = hash!(raw, path).map do |name, value|
|
|
82
|
+
validate_name!(name, "#{path} key")
|
|
83
|
+
stream_path = "#{path}.#{name}"
|
|
84
|
+
stream = hash!(value, stream_path)
|
|
85
|
+
exact_keys!(stream, STREAM_KEYS, stream_path)
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
"name" => name,
|
|
89
|
+
"stream_id" => id!(required(stream, "stream_id", stream_path), "#{stream_path}.stream_id"),
|
|
90
|
+
"default_uri" => validate_uri(stream["default_uri"], "#{stream_path}.default_uri"),
|
|
91
|
+
"enhanced_measurement" => validate_enhanced_measurement(stream["enhanced_measurement"], stream_path)
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
streams.sort_by { |stream| stream.fetch("name") }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def validate_enhanced_measurement(raw, stream_path)
|
|
98
|
+
return nil if raw.nil?
|
|
99
|
+
|
|
100
|
+
path = "#{stream_path}.enhanced_measurement"
|
|
101
|
+
settings = hash!(raw, path)
|
|
102
|
+
exact_keys!(settings, ENHANCED_MEASUREMENT_KEYS, path)
|
|
103
|
+
enabled = boolean!(required(settings, "enabled", path), "#{path}.enabled")
|
|
104
|
+
experimental = boolean!(required(settings, "experimental", path), "#{path}.experimental")
|
|
105
|
+
raise ConfigurationError, "#{path}.experimental must be true" unless experimental
|
|
106
|
+
|
|
107
|
+
{ "enabled" => enabled, "experimental" => true }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def validate_retention(raw, profile_path)
|
|
111
|
+
return nil if raw.nil?
|
|
112
|
+
|
|
113
|
+
path = "#{profile_path}.retention"
|
|
114
|
+
retention = hash!(raw, path)
|
|
115
|
+
exact_keys!(retention, RETENTION_KEYS, path)
|
|
116
|
+
event_data = enum!(required(retention, "event_data", path), RETENTION_VALUES, "#{path}.event_data")
|
|
117
|
+
user_data = enum!(required(retention, "user_data", path), RETENTION_VALUES, "#{path}.user_data")
|
|
118
|
+
reset = boolean!(required(retention, "reset_on_new_activity", path), "#{path}.reset_on_new_activity")
|
|
119
|
+
|
|
120
|
+
{ "event_data" => event_data, "user_data" => user_data, "reset_on_new_activity" => reset }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def validate_google_signals(raw, profile_path)
|
|
124
|
+
return nil if raw.nil?
|
|
125
|
+
|
|
126
|
+
path = "#{profile_path}.google_signals"
|
|
127
|
+
settings = hash!(raw, path)
|
|
128
|
+
exact_keys!(settings, GOOGLE_SIGNALS_KEYS, path)
|
|
129
|
+
state = enum!(required(settings, "state", path), %w[enabled disabled], "#{path}.state")
|
|
130
|
+
experimental = boolean!(required(settings, "experimental", path), "#{path}.experimental")
|
|
131
|
+
raise ConfigurationError, "#{path}.experimental must be true" unless experimental
|
|
132
|
+
|
|
133
|
+
{ "state" => state, "experimental" => true }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def validate_key_events(raw, profile_path)
|
|
137
|
+
path = "#{profile_path}.key_events"
|
|
138
|
+
values = array!(raw, path).map.with_index do |event_name, index|
|
|
139
|
+
string = string!(event_name, "#{profile_path}.key_events[#{index}]")
|
|
140
|
+
unless EVENT_NAME.match?(string)
|
|
141
|
+
raise ConfigurationError, "#{profile_path}.key_events[#{index}] is not a valid GA4 event name"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
string
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
ensure_unique_strings!(values, path)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def validate_dimensions(raw, profile_path)
|
|
151
|
+
path = "#{profile_path}.custom_dimensions"
|
|
152
|
+
values = array!(raw, path).map.with_index do |value, index|
|
|
153
|
+
item_path = "#{path}[#{index}]"
|
|
154
|
+
dimension = hash!(value, item_path)
|
|
155
|
+
exact_keys!(dimension, DIMENSION_KEYS, item_path)
|
|
156
|
+
scope = enum!(required(dimension, "scope", item_path), DIMENSION_SCOPES, "#{item_path}.scope")
|
|
157
|
+
parameter = parameter_name!(required(dimension, "parameter_name", item_path), scope, item_path)
|
|
158
|
+
|
|
159
|
+
{
|
|
160
|
+
"parameter_name" => parameter,
|
|
161
|
+
"display_name" => display_name!(required(dimension, "display_name", item_path), item_path),
|
|
162
|
+
"description" => description!(dimension.fetch("description", ""), item_path),
|
|
163
|
+
"scope" => scope,
|
|
164
|
+
"disallow_ads_personalization" => boolean!(dimension.fetch("disallow_ads_personalization", false),
|
|
165
|
+
"#{item_path}.disallow_ads_personalization")
|
|
166
|
+
}.tap do |normalized|
|
|
167
|
+
if normalized.fetch("disallow_ads_personalization") && scope != "user"
|
|
168
|
+
raise ConfigurationError, "#{item_path}.disallow_ads_personalization is valid only for user scope"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
ensure_unique!(values, %w[scope parameter_name], path)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def validate_metrics(raw, profile_path)
|
|
177
|
+
path = "#{profile_path}.custom_metrics"
|
|
178
|
+
values = array!(raw, path).map.with_index do |value, index|
|
|
179
|
+
item_path = "#{path}[#{index}]"
|
|
180
|
+
metric = hash!(value, item_path)
|
|
181
|
+
exact_keys!(metric, METRIC_KEYS, item_path)
|
|
182
|
+
scope = enum!(required(metric, "scope", item_path), ["event"], "#{item_path}.scope")
|
|
183
|
+
|
|
184
|
+
{
|
|
185
|
+
"parameter_name" => parameter_name!(required(metric, "parameter_name", item_path), scope, item_path),
|
|
186
|
+
"display_name" => display_name!(required(metric, "display_name", item_path), item_path),
|
|
187
|
+
"description" => description!(metric.fetch("description", ""), item_path),
|
|
188
|
+
"scope" => scope,
|
|
189
|
+
"measurement_unit" => enum!(metric.fetch("measurement_unit", "standard"), METRIC_UNITS,
|
|
190
|
+
"#{item_path}.measurement_unit")
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
ensure_unique!(values, ["parameter_name"], path)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def validate_manual_requirements(raw, profile_path)
|
|
198
|
+
path = "#{profile_path}.manual_requirements"
|
|
199
|
+
values = array!(raw, path).map.with_index do |value, index|
|
|
200
|
+
requirement = string!(value, "#{path}[#{index}]")
|
|
201
|
+
validate_name!(requirement, "#{path}[#{index}]")
|
|
202
|
+
requirement
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
ensure_unique_strings!(values, path)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def parameter_name!(value, scope, path)
|
|
209
|
+
parameter = string!(value, "#{path}.parameter_name")
|
|
210
|
+
maximum = scope == "user" ? 24 : 40
|
|
211
|
+
unless NAME.match?(parameter) && parameter.length <= maximum
|
|
212
|
+
raise ConfigurationError,
|
|
213
|
+
"#{path}.parameter_name must start with a letter and be at most #{maximum} characters"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
parameter
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def display_name!(value, path)
|
|
220
|
+
name = string!(value, "#{path}.display_name")
|
|
221
|
+
if name.empty? || name.length > 82 || name.match?(/[\u0000-\u001f]/)
|
|
222
|
+
raise ConfigurationError, "#{path}.display_name must contain 1 to 82 printable characters"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
name
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def description!(value, path)
|
|
229
|
+
description = string!(value, "#{path}.description")
|
|
230
|
+
if description.length > 150 || description.match?(/[\u0000-\u001f\u007f]/)
|
|
231
|
+
raise ConfigurationError, "#{path}.description must be printable and at most 150 characters"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
description
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def validate_uri(value, path)
|
|
238
|
+
return nil if value.nil?
|
|
239
|
+
|
|
240
|
+
string = string!(value, path)
|
|
241
|
+
uri = URI.parse(string)
|
|
242
|
+
unless %w[http https].include?(uri.scheme) && uri.host && !uri.userinfo
|
|
243
|
+
raise ConfigurationError, "#{path} must be an absolute HTTP or HTTPS URI without credentials"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
string
|
|
247
|
+
rescue URI::InvalidURIError
|
|
248
|
+
raise ConfigurationError, "#{path} must be a valid URI"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def reject_secret_keys!(value, path = "$root")
|
|
252
|
+
case value
|
|
253
|
+
when Hash
|
|
254
|
+
value.each do |key, child|
|
|
255
|
+
raise ConfigurationError, "Secret-shaped key #{path}.#{key} is forbidden" if key.to_s.match?(SECRET_KEY)
|
|
256
|
+
|
|
257
|
+
reject_secret_keys!(child, "#{path}.#{key}")
|
|
258
|
+
end
|
|
259
|
+
when Array
|
|
260
|
+
value.each_with_index { |child, index| reject_secret_keys!(child, "#{path}[#{index}]") }
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def exact_keys!(hash, allowed, path)
|
|
265
|
+
keys = hash.keys
|
|
266
|
+
non_strings = keys.grep_v(String)
|
|
267
|
+
raise ConfigurationError, "#{path} keys must be strings" unless non_strings.empty?
|
|
268
|
+
|
|
269
|
+
unknown = keys - allowed
|
|
270
|
+
raise ConfigurationError, "Unknown configuration key #{path}.#{unknown.first}" unless unknown.empty?
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def ensure_unique!(values, keys, path)
|
|
274
|
+
grouped = values.group_by do |value|
|
|
275
|
+
keys.map do |key|
|
|
276
|
+
value.fetch(key)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
duplicate = grouped.find { |_identity, rows| rows.length > 1 }
|
|
280
|
+
raise ConfigurationError, "Duplicate identity in #{path}: #{duplicate.first.join(":")}" if duplicate
|
|
281
|
+
|
|
282
|
+
values.sort_by { |value| keys.map { |key| value.fetch(key) } }
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def ensure_unique_strings!(values, path)
|
|
286
|
+
duplicate = values.tally.find { |_value, count| count > 1 }&.first
|
|
287
|
+
raise ConfigurationError, "Duplicate value in #{path}: #{duplicate}" if duplicate
|
|
288
|
+
|
|
289
|
+
values.sort
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def required(hash, key, path)
|
|
293
|
+
return hash[key] if hash.key?(key)
|
|
294
|
+
|
|
295
|
+
raise ConfigurationError, "Missing required configuration key #{path}.#{key}"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def hash!(value, path)
|
|
299
|
+
return value if value.is_a?(Hash)
|
|
300
|
+
|
|
301
|
+
raise ConfigurationError, "#{path} must be a mapping"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def array!(value, path)
|
|
305
|
+
return value if value.is_a?(Array)
|
|
306
|
+
|
|
307
|
+
raise ConfigurationError, "#{path} must be an array"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def string!(value, path)
|
|
311
|
+
return value if value.is_a?(String)
|
|
312
|
+
|
|
313
|
+
raise ConfigurationError, "#{path} must be a string"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def boolean!(value, path)
|
|
317
|
+
return value if [true, false].include?(value)
|
|
318
|
+
|
|
319
|
+
raise ConfigurationError, "#{path} must be true or false"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def id!(value, path)
|
|
323
|
+
string = string!(value, path)
|
|
324
|
+
raise ConfigurationError, "#{path} must be a numeric identifier encoded as a string" unless ID.match?(string)
|
|
325
|
+
|
|
326
|
+
string
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def enum!(value, allowed, path)
|
|
330
|
+
string = string!(value, path)
|
|
331
|
+
return string if allowed.include?(string)
|
|
332
|
+
|
|
333
|
+
raise ConfigurationError, "#{path} must be one of: #{allowed.join(", ")}"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def validate_name!(value, path)
|
|
337
|
+
string = string!(value, path)
|
|
338
|
+
return string if NAME.match?(string) && string.length <= 64
|
|
339
|
+
|
|
340
|
+
raise ConfigurationError, "#{path} must use letters, numbers, and underscores and start with a letter"
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Safe configuration entry point.
|
|
4
|
+
|
|
5
|
+
require_relative "configuration/document"
|
|
6
|
+
require_relative "configuration/loader"
|
|
7
|
+
require_relative "configuration/validator"
|
|
8
|
+
require_relative "configuration/schema"
|
|
9
|
+
|
|
10
|
+
module AnalyticsOps
|
|
11
|
+
# Strict versioned desired-state loading.
|
|
12
|
+
module Configuration
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def load(path, environment: ENV)
|
|
16
|
+
Loader.new(environment:).load(path)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Validated configuration state.
|
|
4
|
+
|
|
5
|
+
module AnalyticsOps
|
|
6
|
+
# Validated, immutable desired state for one named profile.
|
|
7
|
+
class DesiredState
|
|
8
|
+
attr_reader :profile, :property_id, :streams, :retention, :key_events,
|
|
9
|
+
:custom_dimensions, :custom_metrics, :manual_requirements,
|
|
10
|
+
:google_signals
|
|
11
|
+
|
|
12
|
+
def initialize(profile:, property_id:, streams:, retention:, key_events:, custom_dimensions:, custom_metrics:,
|
|
13
|
+
manual_requirements:, google_signals:)
|
|
14
|
+
@profile = profile.freeze
|
|
15
|
+
@property_id = property_id.freeze
|
|
16
|
+
@streams = Canonical.immutable(streams)
|
|
17
|
+
@retention = Canonical.immutable(retention)
|
|
18
|
+
@key_events = Canonical.immutable(key_events)
|
|
19
|
+
@custom_dimensions = Canonical.immutable(custom_dimensions)
|
|
20
|
+
@custom_metrics = Canonical.immutable(custom_metrics)
|
|
21
|
+
@manual_requirements = Canonical.immutable(manual_requirements)
|
|
22
|
+
@google_signals = Canonical.immutable(google_signals)
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
{
|
|
28
|
+
"profile" => profile,
|
|
29
|
+
"property_id" => property_id,
|
|
30
|
+
"streams" => streams,
|
|
31
|
+
"retention" => retention,
|
|
32
|
+
"key_events" => key_events,
|
|
33
|
+
"custom_dimensions" => custom_dimensions,
|
|
34
|
+
"custom_metrics" => custom_metrics,
|
|
35
|
+
"manual_requirements" => manual_requirements,
|
|
36
|
+
"google_signals" => google_signals
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Public typed errors.
|
|
4
|
+
|
|
5
|
+
module AnalyticsOps
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
class ConfigurationError < Error; end
|
|
8
|
+
class EnvironmentVariableError < ConfigurationError; end
|
|
9
|
+
class UnsupportedVersionError < ConfigurationError; end
|
|
10
|
+
class AuthenticationError < Error; end
|
|
11
|
+
class AuthorizationError < Error; end
|
|
12
|
+
class UnsupportedCapabilityError < Error; end
|
|
13
|
+
class ConflictError < Error; end
|
|
14
|
+
class InvalidPlanError < Error; end
|
|
15
|
+
class StalePlanError < Error; end
|
|
16
|
+
class ConfirmationRequiredError < Error; end
|
|
17
|
+
class QuotaError < Error; end
|
|
18
|
+
class TimeoutError < Error; end
|
|
19
|
+
class InvalidRequestError < Error; end
|
|
20
|
+
class RemoteError < Error; end
|
|
21
|
+
|
|
22
|
+
# Apply failure carrying structured successful, failed, and remaining changes.
|
|
23
|
+
class PartialApplyError < Error
|
|
24
|
+
attr_reader :result
|
|
25
|
+
|
|
26
|
+
def initialize(message, result:)
|
|
27
|
+
@result = result
|
|
28
|
+
super(message)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|