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,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticsOps
|
|
4
|
+
module Clients
|
|
5
|
+
# Narrow adapter around Google's generated Data API client.
|
|
6
|
+
class Data
|
|
7
|
+
PACKAGE_REQUIREMENT = Gem::Requirement.new("~> 0.9.0")
|
|
8
|
+
MATCH_TYPES = {
|
|
9
|
+
"exact" => :EXACT,
|
|
10
|
+
"begins_with" => :BEGINS_WITH,
|
|
11
|
+
"ends_with" => :ENDS_WITH,
|
|
12
|
+
"contains" => :CONTAINS,
|
|
13
|
+
"full_regexp" => :FULL_REGEXP,
|
|
14
|
+
"partial_regexp" => :PARTIAL_REGEXP
|
|
15
|
+
}.freeze
|
|
16
|
+
NUMERIC_OPERATIONS = {
|
|
17
|
+
"equal" => :EQUAL,
|
|
18
|
+
"less_than" => :LESS_THAN,
|
|
19
|
+
"less_than_or_equal" => :LESS_THAN_OR_EQUAL,
|
|
20
|
+
"greater_than" => :GREATER_THAN,
|
|
21
|
+
"greater_than_or_equal" => :GREATER_THAN_OR_EQUAL
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
QUOTA_FIELDS = %i[
|
|
25
|
+
tokens_per_day
|
|
26
|
+
tokens_per_hour
|
|
27
|
+
tokens_per_project_per_hour
|
|
28
|
+
concurrent_requests
|
|
29
|
+
server_errors_per_project_per_hour
|
|
30
|
+
potentially_thresholded_requests_per_hour
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
def initialize(client: nil, credentials: nil, transport: :grpc, timeout: nil, logger: nil)
|
|
34
|
+
transport = transport.to_sym
|
|
35
|
+
raise ConfigurationError, "transport must be grpc or rest" unless %i[grpc rest].include?(transport)
|
|
36
|
+
|
|
37
|
+
@client = client
|
|
38
|
+
@credentials = credentials
|
|
39
|
+
@transport = transport
|
|
40
|
+
@timeout = timeout
|
|
41
|
+
@logger = logger
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run(property_id, definition)
|
|
45
|
+
unless definition.is_a?(Reports::Definition)
|
|
46
|
+
raise InvalidRequestError, "report must be an AnalyticsOps::Reports::Definition"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
response = if definition.realtime?
|
|
50
|
+
invoke(:run_realtime_report, realtime_request(property_id, definition))
|
|
51
|
+
else
|
|
52
|
+
invoke(:run_report, standard_request(property_id, definition))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
normalize_result(definition, response)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def available?
|
|
59
|
+
generated_client = translate_errors { client }
|
|
60
|
+
generated_client.respond_to?(:run_report) && generated_client.respond_to?(:run_realtime_report)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def compatibility
|
|
64
|
+
specification = Gem::Specification.find_by_name("google-analytics-data")
|
|
65
|
+
{
|
|
66
|
+
"package" => specification.name,
|
|
67
|
+
"version" => specification.version.to_s,
|
|
68
|
+
"requirement" => PACKAGE_REQUIREMENT.to_s,
|
|
69
|
+
"supported" => PACKAGE_REQUIREMENT.satisfied_by?(specification.version),
|
|
70
|
+
"transport" => @transport.to_s
|
|
71
|
+
}.freeze
|
|
72
|
+
rescue Gem::LoadError => error
|
|
73
|
+
raise UnsupportedCapabilityError, Redaction.message(error.message)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def client
|
|
79
|
+
@client ||= begin
|
|
80
|
+
require "google/analytics/data"
|
|
81
|
+
Google::Analytics::Data.analytics_data(transport: @transport) do |config|
|
|
82
|
+
config.credentials = @credentials if @credentials
|
|
83
|
+
config.timeout = @timeout if @timeout
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def standard_request(property_id, definition)
|
|
89
|
+
common_request(property_id, definition).merge(
|
|
90
|
+
offset: definition.offset,
|
|
91
|
+
date_ranges: definition.date_ranges.map do |range|
|
|
92
|
+
{
|
|
93
|
+
start_date: range.fetch("start_date"),
|
|
94
|
+
end_date: range.fetch("end_date"),
|
|
95
|
+
name: range["name"]
|
|
96
|
+
}.compact
|
|
97
|
+
end
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def realtime_request(property_id, definition)
|
|
102
|
+
common_request(property_id, definition)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def common_request(property_id, definition)
|
|
106
|
+
{
|
|
107
|
+
property: property_name(property_id),
|
|
108
|
+
dimensions: definition.dimensions.map { |name| { name: } },
|
|
109
|
+
metrics: definition.metrics.map { |name| { name: } },
|
|
110
|
+
dimension_filter: filter_request(definition.dimension_filter),
|
|
111
|
+
metric_filter: metric_filter_request(definition.metric_filter),
|
|
112
|
+
order_bys: definition.order_bys.map { |order| order_request(order) },
|
|
113
|
+
limit: definition.limit,
|
|
114
|
+
return_property_quota: true
|
|
115
|
+
}.compact
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def filter_request(filter)
|
|
119
|
+
return nil unless filter
|
|
120
|
+
|
|
121
|
+
{
|
|
122
|
+
filter: {
|
|
123
|
+
field_name: filter.fetch("field"),
|
|
124
|
+
string_filter: {
|
|
125
|
+
match_type: MATCH_TYPES.fetch(filter.fetch("match_type")),
|
|
126
|
+
value: filter.fetch("value"),
|
|
127
|
+
case_sensitive: filter.fetch("case_sensitive")
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def metric_filter_request(filter)
|
|
134
|
+
return nil unless filter
|
|
135
|
+
|
|
136
|
+
number = filter.fetch("value")
|
|
137
|
+
numeric_value = if number.is_a?(Integer)
|
|
138
|
+
{ int64_value: number }
|
|
139
|
+
else
|
|
140
|
+
{ double_value: number }
|
|
141
|
+
end
|
|
142
|
+
{
|
|
143
|
+
filter: {
|
|
144
|
+
field_name: filter.fetch("field"),
|
|
145
|
+
numeric_filter: {
|
|
146
|
+
operation: NUMERIC_OPERATIONS.fetch(filter.fetch("operation")),
|
|
147
|
+
value: numeric_value
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def order_request(order)
|
|
154
|
+
selector = order.key?("metric") ? :metric : :dimension
|
|
155
|
+
name_key = selector == :metric ? :metric_name : :dimension_name
|
|
156
|
+
{
|
|
157
|
+
selector => { name_key => order.fetch(selector.to_s) },
|
|
158
|
+
desc: order.fetch("desc")
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def invoke(method_name, request)
|
|
163
|
+
SafeLogging.write(
|
|
164
|
+
@logger,
|
|
165
|
+
:info,
|
|
166
|
+
"google_data_request",
|
|
167
|
+
"method" => method_name.to_s,
|
|
168
|
+
"property" => request.fetch(:property)
|
|
169
|
+
)
|
|
170
|
+
generated_client = translate_errors { client }
|
|
171
|
+
unless generated_client.respond_to?(method_name)
|
|
172
|
+
raise UnsupportedCapabilityError, "Installed Google Data client does not support #{method_name}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
translate_errors { generated_client.public_send(method_name, request) }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def translate_errors
|
|
179
|
+
yield
|
|
180
|
+
rescue AnalyticsOps::Error
|
|
181
|
+
raise
|
|
182
|
+
rescue StandardError => error
|
|
183
|
+
translated = case error.class.name
|
|
184
|
+
when /Unauthenticated|Google::Auth|Signet::Authorization/
|
|
185
|
+
AuthenticationError
|
|
186
|
+
when /PermissionDenied|Forbidden/
|
|
187
|
+
AuthorizationError
|
|
188
|
+
when /ResourceExhausted|TooManyRequests/
|
|
189
|
+
QuotaError
|
|
190
|
+
when /DeadlineExceeded|Timeout|ETIMEDOUT/
|
|
191
|
+
TimeoutError
|
|
192
|
+
when /InvalidArgument|FailedPrecondition|NotFound/
|
|
193
|
+
InvalidRequestError
|
|
194
|
+
when /Google::Cloud::|GRPC::|Faraday::|HTTP/
|
|
195
|
+
RemoteError
|
|
196
|
+
end
|
|
197
|
+
raise unless translated
|
|
198
|
+
|
|
199
|
+
raise translated, Redaction.message(error.message)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def normalize_result(definition, response)
|
|
203
|
+
dimension_headers = array_field(response, :dimension_headers).map { |header| field(header, :name).to_s }
|
|
204
|
+
metric_headers = array_field(response, :metric_headers).map { |header| field(header, :name).to_s }
|
|
205
|
+
unless dimension_headers == definition.dimensions && metric_headers == definition.metrics
|
|
206
|
+
raise RemoteError, "Google Data API returned headers that do not match the requested report"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
rows = array_field(response, :rows).map do |row|
|
|
210
|
+
dimensions = values(row, :dimension_values)
|
|
211
|
+
metrics = values(row, :metric_values)
|
|
212
|
+
unless dimensions.length == dimension_headers.length && metrics.length == metric_headers.length
|
|
213
|
+
raise RemoteError, "Google Data API returned a row that does not match its headers"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
dimension_headers.zip(dimensions).to_h.merge(metric_headers.zip(metrics).to_h)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
Reports::Result.new(
|
|
220
|
+
name: definition.name,
|
|
221
|
+
kind: definition.kind,
|
|
222
|
+
dimension_headers:,
|
|
223
|
+
metric_headers:,
|
|
224
|
+
rows:,
|
|
225
|
+
row_count: response_row_count(response, rows.length),
|
|
226
|
+
metadata: metadata(response)
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def values(row, name)
|
|
231
|
+
array_field(row, name).map { |value| field(value, :value).to_s }
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def metadata(response)
|
|
235
|
+
response_metadata = field(response, :metadata)
|
|
236
|
+
result = {
|
|
237
|
+
"data_loss_from_other_row" => boolean?(field(response_metadata, :data_loss_from_other_row)),
|
|
238
|
+
"subject_to_thresholding" => boolean?(field(response_metadata, :subject_to_thresholding)),
|
|
239
|
+
"currency_code" => optional_string(field(response_metadata, :currency_code)),
|
|
240
|
+
"time_zone" => optional_string(field(response_metadata, :time_zone)),
|
|
241
|
+
"empty_reason" => optional_string(field(response_metadata, :empty_reason)),
|
|
242
|
+
"sampling" => sampling(response_metadata),
|
|
243
|
+
"property_quota" => quota(field(response, :property_quota))
|
|
244
|
+
}
|
|
245
|
+
result.reject { |_key, value| value.nil? || value == [] }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def sampling(response_metadata)
|
|
249
|
+
array_field(response_metadata, :sampling_metadatas).map do |sample|
|
|
250
|
+
{
|
|
251
|
+
"samples_read_count" => integer(field(sample, :samples_read_count), 0),
|
|
252
|
+
"sampling_space_size" => integer(field(sample, :sampling_space_size), 0)
|
|
253
|
+
}
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def quota(value)
|
|
258
|
+
return nil unless value
|
|
259
|
+
|
|
260
|
+
QUOTA_FIELDS.each_with_object({}) do |name, result|
|
|
261
|
+
status = field(value, name)
|
|
262
|
+
next unless status
|
|
263
|
+
|
|
264
|
+
result[name.to_s] = {
|
|
265
|
+
"consumed" => integer(field(status, :consumed), 0),
|
|
266
|
+
"remaining" => integer(field(status, :remaining), 0)
|
|
267
|
+
}
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def property_name(property_id)
|
|
272
|
+
id = property_id.to_s
|
|
273
|
+
raise ConfigurationError, "Invalid property ID" unless id.match?(/\A\d{1,50}\z/)
|
|
274
|
+
|
|
275
|
+
"properties/#{id}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def field(value, name)
|
|
279
|
+
return nil if value.nil?
|
|
280
|
+
return value.public_send(name) if value.respond_to?(name)
|
|
281
|
+
return value[name] if value.respond_to?(:key?) && value.key?(name)
|
|
282
|
+
return value[name.to_s] if value.respond_to?(:key?) && value.key?(name.to_s)
|
|
283
|
+
|
|
284
|
+
nil
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def array_field(value, name)
|
|
288
|
+
Array(field(value, name))
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def optional_string(value)
|
|
292
|
+
string = value.to_s
|
|
293
|
+
string.empty? ? nil : string
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def integer(value, default)
|
|
297
|
+
value.nil? ? default : Integer(value)
|
|
298
|
+
rescue ArgumentError, TypeError
|
|
299
|
+
default
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def response_row_count(response, default)
|
|
303
|
+
value = field(response, :row_count)
|
|
304
|
+
value.nil? ? default : Integer(value)
|
|
305
|
+
rescue ArgumentError, TypeError
|
|
306
|
+
raise RemoteError, "Google Data API returned an invalid row count"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def boolean?(value)
|
|
310
|
+
value == true
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Immutable configuration document.
|
|
4
|
+
|
|
5
|
+
module AnalyticsOps
|
|
6
|
+
module Configuration
|
|
7
|
+
# Immutable collection of validated named profiles.
|
|
8
|
+
class Document
|
|
9
|
+
attr_reader :version, :profiles
|
|
10
|
+
|
|
11
|
+
def initialize(version:, profiles:)
|
|
12
|
+
@version = version
|
|
13
|
+
@profiles = Canonical.deep_freeze(profiles)
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def profile(name)
|
|
18
|
+
profiles.fetch(name.to_s) do
|
|
19
|
+
available = profiles.keys.sort.join(", ")
|
|
20
|
+
raise ConfigurationError, "Unknown profile #{name.inspect}; available profiles: #{available}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Safe YAML loading implementation.
|
|
4
|
+
|
|
5
|
+
require "psych"
|
|
6
|
+
|
|
7
|
+
module AnalyticsOps
|
|
8
|
+
module Configuration
|
|
9
|
+
# Bounded safe-YAML reader with allowlisted environment interpolation.
|
|
10
|
+
class Loader
|
|
11
|
+
MAX_BYTES = 1_048_576
|
|
12
|
+
VARIABLE = /\$\{([A-Z][A-Z0-9_]*)\}/
|
|
13
|
+
|
|
14
|
+
def initialize(environment: ENV)
|
|
15
|
+
@environment = environment
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def load(path)
|
|
19
|
+
source = read(path)
|
|
20
|
+
raise ConfigurationError, "ERB is not allowed in Analytics Ops configuration" if source.include?("<%")
|
|
21
|
+
|
|
22
|
+
parsed = Psych.safe_load(
|
|
23
|
+
source,
|
|
24
|
+
permitted_classes: [],
|
|
25
|
+
permitted_symbols: [],
|
|
26
|
+
aliases: false,
|
|
27
|
+
filename: path.to_s,
|
|
28
|
+
fallback: {}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
Validator.new(interpolate(parsed)).call
|
|
32
|
+
rescue Psych::Exception => error
|
|
33
|
+
raise ConfigurationError, "Invalid YAML in #{path}: #{error.message}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def read(path)
|
|
39
|
+
contents = File.binread(path, MAX_BYTES + 1)
|
|
40
|
+
raise ConfigurationError, "Configuration exceeds #{MAX_BYTES} bytes" if contents.bytesize > MAX_BYTES
|
|
41
|
+
|
|
42
|
+
contents
|
|
43
|
+
rescue SystemCallError => error
|
|
44
|
+
raise ConfigurationError, "Cannot read configuration #{path}: #{error.message}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def interpolate(value)
|
|
48
|
+
case value
|
|
49
|
+
when Hash
|
|
50
|
+
value.to_h { |key, child| [key, interpolate(child)] }
|
|
51
|
+
when Array
|
|
52
|
+
value.map { |child| interpolate(child) }
|
|
53
|
+
when String
|
|
54
|
+
interpolate_string(value)
|
|
55
|
+
else
|
|
56
|
+
value
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def interpolate_string(value)
|
|
61
|
+
result = value.gsub(VARIABLE) do
|
|
62
|
+
name = Regexp.last_match(1)
|
|
63
|
+
raise EnvironmentVariableError, "Missing environment variable #{name}" unless @environment.key?(name)
|
|
64
|
+
|
|
65
|
+
@environment.fetch(name).to_s
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if result.include?("${")
|
|
69
|
+
raise EnvironmentVariableError, "Malformed environment interpolation in #{value.inspect}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
result
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticsOps
|
|
4
|
+
module Configuration
|
|
5
|
+
# Public machine-readable summary. Runtime validation also enforces
|
|
6
|
+
# cross-field identity rules and printable user-visible values.
|
|
7
|
+
SCHEMA = Canonical.deep_freeze(
|
|
8
|
+
"$schema" => "https://json-schema.org/draft/2020-12/schema",
|
|
9
|
+
"$id" => "https://github.com/nelsonchaves/analytics_ops/blob/master/docs/configuration-schema-v1.json",
|
|
10
|
+
"title" => "Analytics Ops configuration version 1",
|
|
11
|
+
"type" => "object",
|
|
12
|
+
"additionalProperties" => false,
|
|
13
|
+
"required" => %w[version profiles],
|
|
14
|
+
"properties" => {
|
|
15
|
+
"version" => { "const" => 1 },
|
|
16
|
+
"profiles" => {
|
|
17
|
+
"type" => "object",
|
|
18
|
+
"minProperties" => 1,
|
|
19
|
+
"propertyNames" => { "pattern" => "^[A-Za-z][A-Za-z0-9_]{0,63}$" },
|
|
20
|
+
"additionalProperties" => { "$ref" => "#/$defs/profile" }
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"$defs" => {
|
|
24
|
+
"id" => {
|
|
25
|
+
"type" => "string",
|
|
26
|
+
"pattern" => "^(?:[0-9]{1,50}|\\$\\{[A-Z][A-Z0-9_]*\\})$"
|
|
27
|
+
},
|
|
28
|
+
"profile" => {
|
|
29
|
+
"type" => "object",
|
|
30
|
+
"additionalProperties" => false,
|
|
31
|
+
"required" => ["property_id"],
|
|
32
|
+
"properties" => {
|
|
33
|
+
"property_id" => { "$ref" => "#/$defs/id" },
|
|
34
|
+
"streams" => {
|
|
35
|
+
"type" => "object",
|
|
36
|
+
"propertyNames" => { "pattern" => "^[A-Za-z][A-Za-z0-9_]{0,63}$" },
|
|
37
|
+
"additionalProperties" => { "$ref" => "#/$defs/stream" }
|
|
38
|
+
},
|
|
39
|
+
"retention" => { "$ref" => "#/$defs/retention" },
|
|
40
|
+
"google_signals" => { "$ref" => "#/$defs/googleSignals" },
|
|
41
|
+
"key_events" => {
|
|
42
|
+
"type" => "array", "uniqueItems" => true,
|
|
43
|
+
"items" => { "type" => "string", "pattern" => "^[A-Za-z][A-Za-z0-9_]{0,39}$" }
|
|
44
|
+
},
|
|
45
|
+
"custom_dimensions" => { "type" => "array", "items" => { "$ref" => "#/$defs/customDimension" } },
|
|
46
|
+
"custom_metrics" => { "type" => "array", "items" => { "$ref" => "#/$defs/customMetric" } },
|
|
47
|
+
"manual_requirements" => {
|
|
48
|
+
"type" => "array", "uniqueItems" => true,
|
|
49
|
+
"items" => { "type" => "string", "pattern" => "^[A-Za-z][A-Za-z0-9_]{0,63}$" }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"stream" => {
|
|
54
|
+
"type" => "object",
|
|
55
|
+
"additionalProperties" => false,
|
|
56
|
+
"required" => ["stream_id"],
|
|
57
|
+
"properties" => {
|
|
58
|
+
"stream_id" => { "$ref" => "#/$defs/id" },
|
|
59
|
+
"default_uri" => { "type" => "string", "maxLength" => 2_048, "pattern" => "^(?:https?://|\\$\\{)" },
|
|
60
|
+
"enhanced_measurement" => {
|
|
61
|
+
"type" => "object", "additionalProperties" => false,
|
|
62
|
+
"required" => %w[enabled experimental],
|
|
63
|
+
"properties" => { "enabled" => { "type" => "boolean" }, "experimental" => { "const" => true } }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"retention" => {
|
|
68
|
+
"type" => "object", "additionalProperties" => false,
|
|
69
|
+
"required" => %w[event_data user_data reset_on_new_activity],
|
|
70
|
+
"properties" => {
|
|
71
|
+
"event_data" => { "$ref" => "#/$defs/retentionValue" },
|
|
72
|
+
"user_data" => { "$ref" => "#/$defs/retentionValue" },
|
|
73
|
+
"reset_on_new_activity" => { "type" => "boolean" }
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"retentionValue" => { "enum" => Configuration::Validator::RETENTION_VALUES },
|
|
77
|
+
"googleSignals" => {
|
|
78
|
+
"type" => "object", "additionalProperties" => false,
|
|
79
|
+
"required" => %w[state experimental],
|
|
80
|
+
"properties" => { "state" => { "enum" => %w[enabled disabled] }, "experimental" => { "const" => true } }
|
|
81
|
+
},
|
|
82
|
+
"customDimension" => {
|
|
83
|
+
"type" => "object", "additionalProperties" => false,
|
|
84
|
+
"required" => %w[parameter_name display_name scope],
|
|
85
|
+
"properties" => {
|
|
86
|
+
"parameter_name" => { "type" => "string", "pattern" => "^[A-Za-z][A-Za-z0-9_]{0,39}$" },
|
|
87
|
+
"display_name" => { "type" => "string", "minLength" => 1, "maxLength" => 82 },
|
|
88
|
+
"description" => { "type" => "string", "maxLength" => 150 },
|
|
89
|
+
"scope" => { "enum" => Configuration::Validator::DIMENSION_SCOPES },
|
|
90
|
+
"disallow_ads_personalization" => { "type" => "boolean" }
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"customMetric" => {
|
|
94
|
+
"type" => "object", "additionalProperties" => false,
|
|
95
|
+
"required" => %w[parameter_name display_name scope],
|
|
96
|
+
"properties" => {
|
|
97
|
+
"parameter_name" => { "type" => "string", "pattern" => "^[A-Za-z][A-Za-z0-9_]{0,39}$" },
|
|
98
|
+
"display_name" => { "type" => "string", "minLength" => 1, "maxLength" => 82 },
|
|
99
|
+
"description" => { "type" => "string", "maxLength" => 150 },
|
|
100
|
+
"scope" => { "const" => "event" },
|
|
101
|
+
"measurement_unit" => { "enum" => Configuration::Validator::METRIC_UNITS }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|