analytics_ops 0.1.0 → 0.2.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -0
  3. data/CONTRIBUTING.md +2 -1
  4. data/README.md +232 -41
  5. data/SECURITY.md +8 -8
  6. data/docs/api-support-matrix.md +9 -8
  7. data/docs/architecture.md +22 -4
  8. data/docs/authentication.md +112 -51
  9. data/docs/commands.md +71 -17
  10. data/docs/configuration-schema-v1.json +53 -7
  11. data/docs/configuration.md +33 -9
  12. data/docs/google-client-compatibility.md +10 -6
  13. data/docs/live-smoke-test.md +159 -0
  14. data/docs/plan-format.md +4 -0
  15. data/docs/plan-schema-v1.json +113 -22
  16. data/docs/rails.md +12 -3
  17. data/docs/reports.md +44 -6
  18. data/docs/safety.md +18 -6
  19. data/docs/troubleshooting.md +42 -13
  20. data/lib/analytics_ops/applier.rb +2 -1
  21. data/lib/analytics_ops/cli/options.rb +157 -0
  22. data/lib/analytics_ops/cli/presenter.rb +221 -0
  23. data/lib/analytics_ops/cli.rb +172 -201
  24. data/lib/analytics_ops/clients/admin.rb +130 -129
  25. data/lib/analytics_ops/clients/admin_normalization.rb +95 -0
  26. data/lib/analytics_ops/clients/data.rb +120 -63
  27. data/lib/analytics_ops/clients/error_translation.rb +45 -0
  28. data/lib/analytics_ops/configuration/document.rb +1 -1
  29. data/lib/analytics_ops/configuration/loader.rb +38 -5
  30. data/lib/analytics_ops/configuration/schema.rb +51 -8
  31. data/lib/analytics_ops/configuration/validator.rb +43 -8
  32. data/lib/analytics_ops/configuration/writer.rb +105 -0
  33. data/lib/analytics_ops/configuration.rb +1 -0
  34. data/lib/analytics_ops/connection.rb +93 -0
  35. data/lib/analytics_ops/desired_state.rb +2 -2
  36. data/lib/analytics_ops/plan.rb +94 -33
  37. data/lib/analytics_ops/planner.rb +10 -2
  38. data/lib/analytics_ops/rails/railtie.rb +13 -18
  39. data/lib/analytics_ops/redaction.rb +14 -9
  40. data/lib/analytics_ops/reports/catalog.rb +22 -5
  41. data/lib/analytics_ops/reports/definition.rb +74 -37
  42. data/lib/analytics_ops/reports/overview.rb +55 -0
  43. data/lib/analytics_ops/reports/overview_result.rb +54 -0
  44. data/lib/analytics_ops/reports/result.rb +38 -9
  45. data/lib/analytics_ops/reports.rb +2 -0
  46. data/lib/analytics_ops/resources.rb +2 -1
  47. data/lib/analytics_ops/service_account.rb +171 -0
  48. data/lib/analytics_ops/setup.rb +171 -0
  49. data/lib/analytics_ops/snapshot.rb +20 -5
  50. data/lib/analytics_ops/version.rb +1 -1
  51. data/lib/analytics_ops/workspace.rb +41 -12
  52. data/lib/analytics_ops.rb +4 -0
  53. data/lib/generators/analytics_ops/install_generator.rb +2 -0
  54. data/lib/generators/analytics_ops/templates/analytics_ops.yml +2 -15
  55. data/sig/analytics_ops.rbs +117 -10
  56. metadata +26 -1
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnalyticsOps
4
+ # Interactive or automated property selection after service-account loading.
5
+ class Setup
6
+ PROPERTY_ID = /\A\d{1,50}\z/
7
+ PROFILE = /\A[A-Za-z][A-Za-z0-9_]{0,63}\z/
8
+
9
+ # Immutable setup outcome returned to the CLI and Ruby callers.
10
+ class Result
11
+ attr_reader :config_path, :profile, :property
12
+
13
+ def initialize(config_path:, profile:, property:, created:)
14
+ unless property.is_a?(Resources::Property)
15
+ raise ArgumentError, "property must be an AnalyticsOps::Resources::Property"
16
+ end
17
+ raise ArgumentError, "created must be true or false" unless [true, false].include?(created)
18
+
19
+ @config_path = config_path.to_s.dup.freeze
20
+ @profile = profile.to_s.dup.freeze
21
+ @property = property
22
+ @created = created
23
+ freeze
24
+ end
25
+
26
+ def created?
27
+ @created
28
+ end
29
+
30
+ def status
31
+ created? ? "configured" : "already_configured"
32
+ end
33
+
34
+ def to_h
35
+ {
36
+ "status" => status,
37
+ "config" => config_path,
38
+ "profile" => profile,
39
+ "property" => property.to_h
40
+ }
41
+ end
42
+ end
43
+
44
+ def initialize(connection:, config:, profile:, property_id: nil, noninteractive: false,
45
+ input: $stdin, out: $stdout)
46
+ validate_options!(
47
+ config:,
48
+ profile:,
49
+ property_id:,
50
+ noninteractive:
51
+ )
52
+ @connection = connection
53
+ @config = config
54
+ @profile = profile.to_s
55
+ @property_id = property_id
56
+ @noninteractive = noninteractive
57
+ @input = input
58
+ @out = out
59
+ end
60
+
61
+ def call
62
+ selection = select_property(google_call { @connection.properties })
63
+ verification = google_call { @connection.verify(selection.fetch("id")) }
64
+ write = Configuration::Writer.new.write_minimal(
65
+ @config,
66
+ profile: @profile,
67
+ property_id: verification.property.id
68
+ )
69
+ Result.new(
70
+ config_path: write.path,
71
+ profile: @profile,
72
+ property: verification.property,
73
+ created: write.created?
74
+ )
75
+ end
76
+
77
+ private
78
+
79
+ def validate_options!(config:, profile:, property_id:, noninteractive:)
80
+ validate_config_path!(config)
81
+ validate_profile!(profile)
82
+ validate_property_id!(property_id)
83
+ validate_boolean_option!(noninteractive)
84
+ raise ConfigurationError, "Non-interactive setup requires a property ID" if noninteractive && !property_id
85
+ end
86
+
87
+ def validate_config_path!(config)
88
+ valid = config.is_a?(String) && !config.empty? && !config.match?(/[\u0000-\u001f\u007f]/)
89
+ return if valid
90
+
91
+ raise ConfigurationError, "Setup configuration path is invalid"
92
+ end
93
+
94
+ def validate_profile!(profile)
95
+ raise ConfigurationError, "Setup profile is invalid" unless PROFILE.match?(profile.to_s)
96
+ end
97
+
98
+ def validate_property_id!(property_id)
99
+ return if property_id.nil? || (property_id.is_a?(String) && PROPERTY_ID.match?(property_id))
100
+
101
+ raise ConfigurationError, "Setup property must be a numeric GA4 property ID"
102
+ end
103
+
104
+ def validate_boolean_option!(noninteractive)
105
+ return if [true, false].include?(noninteractive)
106
+
107
+ raise ConfigurationError, "Setup boolean options must be true or false"
108
+ end
109
+
110
+ def google_call
111
+ yield
112
+ rescue AuthorizationError, InvalidRequestError, RemoteError => error
113
+ raise_disabled_api! if api_disabled?(error)
114
+
115
+ raise
116
+ end
117
+
118
+ def api_disabled?(error)
119
+ error.message.match?(/SERVICE_DISABLED|API.*(?:disabled|not been used)|serviceusage/i)
120
+ end
121
+
122
+ def raise_disabled_api!
123
+ raise RemoteError,
124
+ "Google Analytics APIs appear disabled. Enable Google Analytics Admin API and " \
125
+ "Google Analytics Data API in the Google Cloud project that owns the service account."
126
+ end
127
+
128
+ def select_property(accounts)
129
+ choices = property_choices(accounts)
130
+ raise AuthorizationError, "No accessible Google Analytics properties found" if choices.empty?
131
+
132
+ return explicit_property(choices) if @property_id
133
+ return choices.first if choices.one?
134
+
135
+ prompt_for_property(choices)
136
+ end
137
+
138
+ def explicit_property(choices)
139
+ choices.find { |choice| choice.fetch("id") == @property_id } ||
140
+ raise(AuthorizationError, "Property #{@property_id} is not accessible")
141
+ end
142
+
143
+ def prompt_for_property(choices)
144
+ @out.puts "Choose a Google Analytics property:"
145
+ choices.each.with_index(1) do |choice, index|
146
+ @out.puts " #{index}. #{Redaction.message(choice.fetch("display_name"))} — " \
147
+ "#{Redaction.message(choice.fetch("account_display_name"))} " \
148
+ "(property #{Redaction.message(choice.fetch("id"))})"
149
+ end
150
+ @out.print "Property number: "
151
+ @out.flush
152
+ index = Integer(@input.gets&.strip, exception: false)
153
+ unless index&.between?(1, choices.length)
154
+ raise ConfigurationError, "A property number from 1 to #{choices.length} is required"
155
+ end
156
+
157
+ choices.fetch(index - 1)
158
+ end
159
+
160
+ def property_choices(accounts)
161
+ choices = accounts.flat_map do |account|
162
+ account.properties.map do |property|
163
+ property.merge("account_id" => account.id, "account_display_name" => account.display_name)
164
+ end
165
+ end
166
+ choices.sort_by do |property|
167
+ [property.fetch("account_display_name"), property.fetch("display_name"), property.fetch("id")]
168
+ end
169
+ end
170
+ end
171
+ end
@@ -8,12 +8,23 @@ module AnalyticsOps
8
8
  attr_reader :property, :streams, :retention, :key_events, :custom_dimensions, :custom_metrics
9
9
 
10
10
  def initialize(property:, streams:, retention:, key_events:, custom_dimensions:, custom_metrics:)
11
+ unless property.is_a?(Resources::Property)
12
+ raise ArgumentError, "property must be an AnalyticsOps::Resources::Property"
13
+ end
14
+ unless retention.nil? || retention.is_a?(Resources::Retention)
15
+ raise ArgumentError, "retention must be an AnalyticsOps::Resources::Retention or nil"
16
+ end
17
+
11
18
  @property = property
12
- @streams = sort(streams, &:id)
19
+ @streams = sorted_resources(streams, Resources::DataStream, "streams", &:id)
13
20
  @retention = retention
14
- @key_events = sort(key_events, &:event_name)
15
- @custom_dimensions = sort(custom_dimensions) { |dimension| [dimension.scope, dimension.parameter_name] }
16
- @custom_metrics = sort(custom_metrics, &:parameter_name)
21
+ @key_events = sorted_resources(key_events, Resources::KeyEvent, "key_events", &:event_name)
22
+ @custom_dimensions = sorted_resources(
23
+ custom_dimensions, Resources::CustomDimension, "custom_dimensions"
24
+ ) do |item|
25
+ [item.scope, item.parameter_name]
26
+ end
27
+ @custom_metrics = sorted_resources(custom_metrics, Resources::CustomMetric, "custom_metrics", &:parameter_name)
17
28
  freeze
18
29
  end
19
30
 
@@ -38,7 +49,11 @@ module AnalyticsOps
38
49
 
39
50
  private
40
51
 
41
- def sort(values, &)
52
+ def sorted_resources(values, type, label, &)
53
+ unless values.is_a?(Array) && values.all?(type)
54
+ raise ArgumentError, "#{label} must contain only #{type.name} values"
55
+ end
56
+
42
57
  Canonical.deep_freeze(values.sort_by(&))
43
58
  end
44
59
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AnalyticsOps
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -27,7 +27,7 @@ module AnalyticsOps
27
27
  attr_reader :checks
28
28
 
29
29
  def initialize(checks:)
30
- @checks = Canonical.deep_freeze(checks)
30
+ @checks = Canonical.immutable(checks)
31
31
  freeze
32
32
  end
33
33
 
@@ -42,17 +42,23 @@ module AnalyticsOps
42
42
 
43
43
  attr_reader :desired_state
44
44
 
45
- def self.load(path, profile:, environment: ENV, admin: nil, data: nil, credentials: nil,
45
+ def self.load(path, profile:, environment: ENV, admin: nil, data: nil, service_account: nil,
46
46
  transport: :grpc, timeout: nil, logger: nil)
47
47
  document = Configuration.load(path, environment:)
48
- new(desired_state: document.profile(profile), admin:, data:, credentials:, transport:, timeout:, logger:)
48
+ new(desired_state: document.profile(profile), admin:, data:, service_account:, transport:, timeout:, logger:)
49
49
  end
50
50
 
51
- def initialize(desired_state:, admin: nil, data: nil, credentials: nil, transport: :grpc, timeout: nil, logger: nil)
51
+ def initialize(desired_state:, admin: nil, data: nil, service_account: nil,
52
+ transport: :grpc, timeout: nil, logger: nil)
53
+ unless service_account.nil? || service_account.is_a?(ServiceAccount)
54
+ raise ConfigurationError, "service_account must be an AnalyticsOps::ServiceAccount"
55
+ end
56
+
52
57
  @desired_state = desired_state
53
58
  @admin = admin
59
+ @injected_admin = admin
54
60
  @data = data
55
- @credentials = credentials
61
+ @service_account = service_account
56
62
  @transport = transport
57
63
  @timeout = timeout
58
64
  @logger = logger
@@ -77,7 +83,7 @@ module AnalyticsOps
77
83
  def apply(plan_or_path, confirm: false)
78
84
  saved_plan = plan_or_path.is_a?(Plan) ? plan_or_path : Plan.load(plan_or_path)
79
85
  validate_plan_target!(saved_plan)
80
- Applier.new(admin:).call(saved_plan, confirm:)
86
+ Applier.new(admin: edit_admin).call(saved_plan, confirm:)
81
87
  end
82
88
 
83
89
  def verify
@@ -94,11 +100,17 @@ module AnalyticsOps
94
100
  data.run(desired_state.property_id, definition)
95
101
  end
96
102
 
103
+ def overview
104
+ reports = data.batch(desired_state.property_id, Reports::Catalog.overview)
105
+ Reports::OverviewResult.new(property_id: desired_state.property_id, reports:)
106
+ end
107
+
97
108
  def doctor
98
109
  remote = snapshot
99
110
  checks = [
100
111
  { "name" => "configuration", "status" => "ok", "detail" => "Profile #{desired_state.profile} is valid" },
101
- { "name" => "credentials", "status" => "ok", "detail" => "Google accepted discovered or injected credentials" },
112
+ { "name" => "credentials", "status" => "ok",
113
+ "detail" => "Google accepted the configured service-account credentials" },
102
114
  { "name" => "admin_api", "status" => "ok", "detail" => "Admin API read succeeded" },
103
115
  { "name" => "property_access", "status" => "ok", "detail" => "Read properties/#{remote.property_id}" }
104
116
  ]
@@ -108,9 +120,9 @@ module AnalyticsOps
108
120
  data.run(desired_state.property_id, doctor_report_definition)
109
121
  checks << { "name" => "data_api", "status" => "ok", "detail" => "Data API read succeeded" }
110
122
  checks << {
111
- "name" => "oauth_scopes",
112
- "status" => "unknown",
113
- "detail" => "Installed Google clients do not expose a reliable token-scope inspection contract"
123
+ "name" => "credential_scope",
124
+ "status" => "ok",
125
+ "detail" => "This read-only command uses the Analytics read-only scope"
114
126
  }
115
127
  admin.capabilities.each do |name, available|
116
128
  checks << {
@@ -126,7 +138,8 @@ module AnalyticsOps
126
138
 
127
139
  def admin
128
140
  @admin ||= Clients::Admin.new(
129
- credentials: @credentials,
141
+ service_account:,
142
+ access: :read,
130
143
  transport: @transport,
131
144
  timeout: @timeout,
132
145
  logger: @logger
@@ -135,13 +148,29 @@ module AnalyticsOps
135
148
 
136
149
  def data
137
150
  @data ||= Clients::Data.new(
138
- credentials: @credentials,
151
+ service_account:,
139
152
  transport: @transport,
140
153
  timeout: @timeout,
141
154
  logger: @logger
142
155
  )
143
156
  end
144
157
 
158
+ def edit_admin
159
+ return @injected_admin if @injected_admin
160
+
161
+ @edit_admin ||= Clients::Admin.new(
162
+ service_account:,
163
+ access: :edit,
164
+ transport: @transport,
165
+ timeout: @timeout,
166
+ logger: @logger
167
+ )
168
+ end
169
+
170
+ def service_account
171
+ @service_account ||= ServiceAccount.load
172
+ end
173
+
145
174
  def compatibility_checks
146
175
  [admin.compatibility, data.compatibility].map do |details|
147
176
  {
data/lib/analytics_ops.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "analytics_ops/version"
4
4
  require_relative "analytics_ops/errors"
5
5
  require_relative "analytics_ops/redaction"
6
+ require_relative "analytics_ops/service_account"
6
7
  require_relative "analytics_ops/canonical"
7
8
  require_relative "analytics_ops/resources"
8
9
  require_relative "analytics_ops/desired_state"
@@ -10,9 +11,12 @@ require_relative "analytics_ops/configuration"
10
11
  require_relative "analytics_ops/snapshot"
11
12
  require_relative "analytics_ops/plan"
12
13
  require_relative "analytics_ops/planner"
14
+ require_relative "analytics_ops/clients/error_translation"
13
15
  require_relative "analytics_ops/clients/admin"
14
16
  require_relative "analytics_ops/reports"
15
17
  require_relative "analytics_ops/clients/data"
18
+ require_relative "analytics_ops/connection"
19
+ require_relative "analytics_ops/setup"
16
20
  require_relative "analytics_ops/applier"
17
21
  require_relative "analytics_ops/workspace"
18
22
 
@@ -11,6 +11,8 @@ module AnalyticsOps
11
11
  desc "Create config/analytics_ops.yml and bin/analytics-ops"
12
12
 
13
13
  def copy_configuration
14
+ return if File.exist?(File.join(destination_root, "config/analytics_ops.yml"))
15
+
14
16
  template "analytics_ops.yml", "config/analytics_ops.yml"
15
17
  end
16
18
 
@@ -6,18 +6,5 @@ profiles:
6
6
  # GA4 property ID (numeric), not an account ID, stream ID, or measurement ID.
7
7
  property_id: "${GA4_PROPERTY_ID}"
8
8
 
9
- streams:
10
- web:
11
- # Numeric stream ID from Admin > Data streams, not the G- measurement ID.
12
- stream_id: "${GA4_STREAM_ID}"
13
- default_uri: "https://example.test"
14
-
15
- retention:
16
- event_data: 14_months
17
- user_data: 14_months
18
- reset_on_new_activity: false
19
-
20
- key_events: []
21
- custom_dimensions: []
22
- custom_metrics: []
23
- manual_requirements: []
9
+ # Add managed settings only after reviewing the Analytics Ops configuration guide.
10
+ # This generated starting point produces no mutation plan by itself.
@@ -54,6 +54,30 @@ module AnalyticsOps
54
54
  def initialize: (String message, result: Applier::Result) -> void
55
55
  end
56
56
 
57
+ class ServiceAccount
58
+ MAX_KEY_BYTES: Integer
59
+ READ_SCOPE: String
60
+ EDIT_SCOPE: String
61
+ ACCESS_SCOPES: Hash[Symbol, Array[String]]
62
+
63
+ attr_reader path: String
64
+
65
+ def self.load: (?path: String?, ?store: Store) -> ServiceAccount
66
+ def initialize: (String path) -> void
67
+
68
+ class Store
69
+ VERSION: Integer
70
+ MAX_BYTES: Integer
71
+
72
+ attr_reader path: String
73
+
74
+ def self.default_path: () -> String
75
+ def initialize: (?path: String?) -> void
76
+ def read: () -> String
77
+ def write: (String service_account_path) -> String
78
+ end
79
+ end
80
+
57
81
  module Resources
58
82
  class Value
59
83
  def to_h: () -> record
@@ -116,6 +140,7 @@ module AnalyticsOps
116
140
  attr_reader description: String
117
141
  attr_reader scope: String
118
142
  attr_reader measurement_unit: String
143
+ attr_reader restricted_metric_types: Array[String]
119
144
  end
120
145
  end
121
146
 
@@ -131,6 +156,18 @@ module AnalyticsOps
131
156
  def initialize: (version: Integer, profiles: Hash[String, DesiredState]) -> void
132
157
  def profile: (String | Symbol name) -> DesiredState
133
158
  end
159
+
160
+ class Writer
161
+ class Result
162
+ attr_reader path: String
163
+
164
+ def initialize: (path: String, created: bool) -> void
165
+ def created?: () -> bool
166
+ def to_h: () -> record
167
+ end
168
+
169
+ def write_minimal: (String path, profile: String | Symbol, property_id: String) -> Result
170
+ end
134
171
  end
135
172
 
136
173
  class DesiredState
@@ -277,6 +314,8 @@ module AnalyticsOps
277
314
  module Catalog
278
315
  def self.fetch: (String | Symbol name, ?kind: String?) -> Definition
279
316
  def self.names: () -> Array[String]
317
+ def self.aliases: () -> Hash[String, String]
318
+ def self.overview: () -> Array[Definition]
280
319
  end
281
320
 
282
321
  class Result
@@ -300,18 +339,31 @@ module AnalyticsOps
300
339
  def headers: () -> Array[String]
301
340
  def to_h: () -> record
302
341
  end
342
+
343
+ class OverviewResult
344
+ MAX_REPORTS: Integer
345
+
346
+ attr_reader property_id: String
347
+ attr_reader reports: Array[Result]
348
+ attr_reader property_quota: record
349
+
350
+ def initialize: (property_id: String, reports: Array[Result]) -> void
351
+ def report: (String | Symbol name) -> Result
352
+ def to_h: () -> record
353
+ end
303
354
  end
304
355
 
305
356
  module Clients
306
357
  class Admin
307
358
  def initialize: (
308
359
  ?client: untyped,
309
- ?credentials: untyped,
360
+ ?service_account: ServiceAccount?,
361
+ ?access: :read | :edit,
310
362
  ?transport: Symbol,
311
- ?timeout: Float?,
363
+ ?timeout: (Integer | Float)?,
312
364
  ?logger: untyped
313
365
  ) -> void
314
- def discover: () -> Array[Resources::Account]
366
+ def discover: (?include_streams: bool) -> Array[Resources::Account]
315
367
  def property_access: (String property_id) -> Resources::Property
316
368
  def snapshot: (String property_id) -> Snapshot
317
369
  def capabilities: () -> Hash[String, bool]
@@ -322,17 +374,68 @@ module AnalyticsOps
322
374
  class Data
323
375
  def initialize: (
324
376
  ?client: untyped,
325
- ?credentials: untyped,
377
+ ?service_account: ServiceAccount?,
326
378
  ?transport: Symbol,
327
- ?timeout: Float?,
379
+ ?timeout: (Integer | Float)?,
328
380
  ?logger: untyped
329
381
  ) -> void
330
382
  def run: (String property_id, Reports::Definition definition) -> Reports::Result
383
+ def batch: (String property_id, Array[Reports::Definition] definitions) -> Array[Reports::Result]
331
384
  def available?: () -> bool
332
385
  def compatibility: () -> record
333
386
  end
334
387
  end
335
388
 
389
+ class Connection
390
+ class Verification
391
+ attr_reader property: Resources::Property
392
+
393
+ def initialize: (property: Resources::Property) -> void
394
+ def to_h: () -> record
395
+ end
396
+
397
+ def initialize: (
398
+ ?admin: Clients::Admin?,
399
+ ?data: Clients::Data?,
400
+ ?service_account: ServiceAccount?,
401
+ ?transport: Symbol,
402
+ ?timeout: (Integer | Float)?,
403
+ ?logger: untyped
404
+ ) -> void
405
+ def discover: () -> Array[Resources::Account]
406
+ def properties: () -> Array[Resources::Account]
407
+ def verify: (String property_id) -> Verification
408
+ end
409
+
410
+ class Setup
411
+ class Result
412
+ attr_reader config_path: String
413
+ attr_reader profile: String
414
+ attr_reader property: Resources::Property
415
+
416
+ def initialize: (
417
+ config_path: String,
418
+ profile: String | Symbol,
419
+ property: Resources::Property,
420
+ created: bool
421
+ ) -> void
422
+ def created?: () -> bool
423
+ def status: () -> String
424
+ def to_h: () -> record
425
+ end
426
+
427
+ def initialize: (
428
+ connection: Connection,
429
+ config: String,
430
+ profile: String | Symbol,
431
+ ?property_id: String?,
432
+ ?noninteractive: bool,
433
+ ?input: untyped,
434
+ ?out: untyped
435
+ ) -> void
436
+ def call: () -> Result
437
+ end
438
+
336
439
  class Workspace
337
440
  class Verification
338
441
  attr_reader converged: bool
@@ -358,18 +461,18 @@ module AnalyticsOps
358
461
  ?environment: Hash[String, String],
359
462
  ?admin: Clients::Admin?,
360
463
  ?data: Clients::Data?,
361
- ?credentials: untyped,
464
+ ?service_account: ServiceAccount?,
362
465
  ?transport: Symbol,
363
- ?timeout: Float?,
466
+ ?timeout: (Integer | Float)?,
364
467
  ?logger: untyped
365
468
  ) -> Workspace
366
469
  def initialize: (
367
470
  desired_state: DesiredState,
368
471
  ?admin: Clients::Admin?,
369
472
  ?data: Clients::Data?,
370
- ?credentials: untyped,
473
+ ?service_account: ServiceAccount?,
371
474
  ?transport: Symbol,
372
- ?timeout: Float?,
475
+ ?timeout: (Integer | Float)?,
373
476
  ?logger: untyped
374
477
  ) -> void
375
478
  def discover: () -> Array[Resources::Account]
@@ -380,6 +483,7 @@ module AnalyticsOps
380
483
  def verify: () -> Verification
381
484
  def report: (String | Reports::Definition name_or_definition) -> Reports::Result
382
485
  def realtime: (?String | Reports::Definition name_or_definition) -> Reports::Result
486
+ def overview: () -> Reports::OverviewResult
383
487
  def doctor: () -> DoctorResult
384
488
  end
385
489
 
@@ -389,7 +493,10 @@ module AnalyticsOps
389
493
  ?out: untyped,
390
494
  ?err: untyped,
391
495
  ?input: untyped,
392
- ?workspace_loader: untyped
496
+ ?workspace_loader: untyped,
497
+ ?connection_loader: untyped,
498
+ ?service_account_loader: untyped,
499
+ ?service_account_store: ServiceAccount::Store?
393
500
  ) -> Integer
394
501
  end
395
502
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: analytics_ops
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nelson Chaves
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: 0.9.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: googleauth
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.12'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.12'
40
54
  description: |-
41
55
  Analytics Ops provides configuration-as-code, drift detection, explicit
42
56
  plans, safe application, and reporting for Google Analytics 4 properties.
@@ -62,6 +76,7 @@ files:
62
76
  - docs/configuration-schema-v1.json
63
77
  - docs/configuration.md
64
78
  - docs/google-client-compatibility.md
79
+ - docs/live-smoke-test.md
65
80
  - docs/plan-format.md
66
81
  - docs/plan-schema-v1.json
67
82
  - docs/rails.md
@@ -73,13 +88,19 @@ files:
73
88
  - lib/analytics_ops/applier.rb
74
89
  - lib/analytics_ops/canonical.rb
75
90
  - lib/analytics_ops/cli.rb
91
+ - lib/analytics_ops/cli/options.rb
92
+ - lib/analytics_ops/cli/presenter.rb
76
93
  - lib/analytics_ops/clients/admin.rb
94
+ - lib/analytics_ops/clients/admin_normalization.rb
77
95
  - lib/analytics_ops/clients/data.rb
96
+ - lib/analytics_ops/clients/error_translation.rb
78
97
  - lib/analytics_ops/configuration.rb
79
98
  - lib/analytics_ops/configuration/document.rb
80
99
  - lib/analytics_ops/configuration/loader.rb
81
100
  - lib/analytics_ops/configuration/schema.rb
82
101
  - lib/analytics_ops/configuration/validator.rb
102
+ - lib/analytics_ops/configuration/writer.rb
103
+ - lib/analytics_ops/connection.rb
83
104
  - lib/analytics_ops/desired_state.rb
84
105
  - lib/analytics_ops/errors.rb
85
106
  - lib/analytics_ops/plan.rb
@@ -90,8 +111,12 @@ files:
90
111
  - lib/analytics_ops/reports.rb
91
112
  - lib/analytics_ops/reports/catalog.rb
92
113
  - lib/analytics_ops/reports/definition.rb
114
+ - lib/analytics_ops/reports/overview.rb
115
+ - lib/analytics_ops/reports/overview_result.rb
93
116
  - lib/analytics_ops/reports/result.rb
94
117
  - lib/analytics_ops/resources.rb
118
+ - lib/analytics_ops/service_account.rb
119
+ - lib/analytics_ops/setup.rb
95
120
  - lib/analytics_ops/snapshot.rb
96
121
  - lib/analytics_ops/version.rb
97
122
  - lib/analytics_ops/workspace.rb