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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +76 -0
- data/CONTRIBUTING.md +2 -1
- data/README.md +232 -41
- data/SECURITY.md +8 -8
- data/docs/api-support-matrix.md +9 -8
- data/docs/architecture.md +22 -4
- data/docs/authentication.md +112 -51
- data/docs/commands.md +71 -17
- data/docs/configuration-schema-v1.json +53 -7
- data/docs/configuration.md +33 -9
- data/docs/google-client-compatibility.md +10 -6
- data/docs/live-smoke-test.md +159 -0
- data/docs/plan-format.md +4 -0
- data/docs/plan-schema-v1.json +113 -22
- data/docs/rails.md +12 -3
- data/docs/reports.md +44 -6
- data/docs/safety.md +18 -6
- data/docs/troubleshooting.md +42 -13
- data/lib/analytics_ops/applier.rb +2 -1
- data/lib/analytics_ops/cli/options.rb +157 -0
- data/lib/analytics_ops/cli/presenter.rb +221 -0
- data/lib/analytics_ops/cli.rb +172 -201
- data/lib/analytics_ops/clients/admin.rb +130 -129
- data/lib/analytics_ops/clients/admin_normalization.rb +95 -0
- data/lib/analytics_ops/clients/data.rb +120 -63
- data/lib/analytics_ops/clients/error_translation.rb +45 -0
- data/lib/analytics_ops/configuration/document.rb +1 -1
- data/lib/analytics_ops/configuration/loader.rb +38 -5
- data/lib/analytics_ops/configuration/schema.rb +51 -8
- data/lib/analytics_ops/configuration/validator.rb +43 -8
- data/lib/analytics_ops/configuration/writer.rb +105 -0
- data/lib/analytics_ops/configuration.rb +1 -0
- data/lib/analytics_ops/connection.rb +93 -0
- data/lib/analytics_ops/desired_state.rb +2 -2
- data/lib/analytics_ops/plan.rb +94 -33
- data/lib/analytics_ops/planner.rb +10 -2
- data/lib/analytics_ops/rails/railtie.rb +13 -18
- data/lib/analytics_ops/redaction.rb +14 -9
- data/lib/analytics_ops/reports/catalog.rb +22 -5
- data/lib/analytics_ops/reports/definition.rb +74 -37
- data/lib/analytics_ops/reports/overview.rb +55 -0
- data/lib/analytics_ops/reports/overview_result.rb +54 -0
- data/lib/analytics_ops/reports/result.rb +38 -9
- data/lib/analytics_ops/reports.rb +2 -0
- data/lib/analytics_ops/resources.rb +2 -1
- data/lib/analytics_ops/service_account.rb +171 -0
- data/lib/analytics_ops/setup.rb +171 -0
- data/lib/analytics_ops/snapshot.rb +20 -5
- data/lib/analytics_ops/version.rb +1 -1
- data/lib/analytics_ops/workspace.rb +41 -12
- data/lib/analytics_ops.rb +4 -0
- data/lib/generators/analytics_ops/install_generator.rb +2 -0
- data/lib/generators/analytics_ops/templates/analytics_ops.yml +2 -15
- data/sig/analytics_ops.rbs +117 -10
- metadata +26 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Live read-only smoke test
|
|
2
|
+
|
|
3
|
+
Use this guide before releasing Analytics Ops or when checking it in a real
|
|
4
|
+
application. The test reads Google Analytics and writes local setup files. It
|
|
5
|
+
never changes GA4.
|
|
6
|
+
|
|
7
|
+
## What you need
|
|
8
|
+
|
|
9
|
+
| Item | Purpose | Fake example |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Google account | Your human login for Cloud and GA4 administration | `operator@example.test` |
|
|
12
|
+
| GA4 account | Owns one or more GA4 properties | Account ID `100000001` |
|
|
13
|
+
| GA4 property | Holds the configuration and reports being tested | Property ID `123456789` |
|
|
14
|
+
| Google Cloud project | Owns API enablement, quota, and the service account | `example-analytics-project` |
|
|
15
|
+
| Service account | The non-human identity Analytics Ops uses | `analytics-ops@example-project.iam.gserviceaccount.com` |
|
|
16
|
+
|
|
17
|
+
Cloud IAM access and GA4 access are separate. The service account does not
|
|
18
|
+
need a broad Cloud role, but its email must be added inside GA4 Access
|
|
19
|
+
Management.
|
|
20
|
+
|
|
21
|
+
A normal API key cannot read private GA4 data or change GA4 settings.
|
|
22
|
+
|
|
23
|
+
## Prepare Google once
|
|
24
|
+
|
|
25
|
+
1. In the Cloud project, enable:
|
|
26
|
+
- Google Analytics Admin API
|
|
27
|
+
- Google Analytics Data API
|
|
28
|
+
2. Open [Google Cloud service accounts](https://console.cloud.google.com/iam-admin/serviceaccounts).
|
|
29
|
+
3. Create **Analytics Ops Local** without broad Cloud IAM roles.
|
|
30
|
+
4. Create a JSON key and save it outside all repositories.
|
|
31
|
+
5. In GA4, open **Admin → Account access management → Add users**.
|
|
32
|
+
6. Add the service-account email as Viewer for read-only use or Editor for
|
|
33
|
+
future reviewed applies.
|
|
34
|
+
|
|
35
|
+
No Google Cloud CLI, browser authorization, consent screen, OAuth client, or
|
|
36
|
+
test-user setup is used.
|
|
37
|
+
|
|
38
|
+
## Test the checkout through a real app
|
|
39
|
+
|
|
40
|
+
Temporarily point the host application's Gemfile at the local gem:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
gem "analytics_ops",
|
|
44
|
+
path: "/absolute/path/to/analytics_ops",
|
|
45
|
+
group: :development
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Run `bundle install`. `bundle info analytics_ops` must show the local checkout
|
|
49
|
+
and version `0.2.0`.
|
|
50
|
+
|
|
51
|
+
### Preserve an existing configuration
|
|
52
|
+
|
|
53
|
+
Setup never overwrites a conflicting profile. If
|
|
54
|
+
`config/analytics_ops.yml` already exists, hold it safely outside the
|
|
55
|
+
application while testing:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
analytics_smoke_backup="$(mktemp -d)"
|
|
59
|
+
cp config/analytics_ops.yml "$analytics_smoke_backup/original.yml"
|
|
60
|
+
mv config/analytics_ops.yml "$analytics_smoke_backup/held.yml"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Do not print the file. Production identifiers and environment conventions do
|
|
64
|
+
not belong in public test output.
|
|
65
|
+
|
|
66
|
+
### Connect once
|
|
67
|
+
|
|
68
|
+
Use the downloaded key without printing its contents:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bundle exec analytics-ops setup \
|
|
72
|
+
--service-account /absolute/path/outside/repositories/service-account.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Analytics Ops validates the key, verifies both APIs, and remembers only its
|
|
76
|
+
absolute path in `~/.config/analytics_ops/connection.json` with mode `0600`.
|
|
77
|
+
The key is not copied.
|
|
78
|
+
|
|
79
|
+
Run plain setup once more to verify the remembered connection:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
bundle exec analytics-ops setup
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Run every read-only check
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
bundle exec analytics-ops version
|
|
89
|
+
bundle info analytics_ops
|
|
90
|
+
bundle exec analytics-ops doctor
|
|
91
|
+
bundle exec analytics-ops overview
|
|
92
|
+
bundle exec analytics-ops report traffic --json
|
|
93
|
+
bundle exec analytics-ops realtime
|
|
94
|
+
bundle exec analytics-ops audit
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Empty reports are acceptable for a property with little traffic.
|
|
98
|
+
Authentication errors, authorization errors, malformed output, crashes, and
|
|
99
|
+
Google-client errors are failures.
|
|
100
|
+
|
|
101
|
+
`audit` exit status `0` means the declared state converges. Exit status `2`
|
|
102
|
+
means drift was found. Both are successful read-only smoke-test outcomes.
|
|
103
|
+
|
|
104
|
+
During the test:
|
|
105
|
+
|
|
106
|
+
- Never run `analytics-ops apply`.
|
|
107
|
+
- Do not create a plan for application.
|
|
108
|
+
- Never commit the service-account JSON, the connection pointer, real report
|
|
109
|
+
rows, screenshots containing credentials, or temporary production IDs.
|
|
110
|
+
- Never paste key contents into a terminal transcript, issue, or chat.
|
|
111
|
+
|
|
112
|
+
## Restore the host app
|
|
113
|
+
|
|
114
|
+
Restore the original configuration:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
mv config/analytics_ops.yml "$analytics_smoke_backup/generated.yml"
|
|
118
|
+
cp "$analytics_smoke_backup/original.yml" config/analytics_ops.yml
|
|
119
|
+
cmp "$analytics_smoke_backup/original.yml" config/analytics_ops.yml
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
After confirming the restoration, remove the temporary backup with the
|
|
123
|
+
operating system's normal secure cleanup process.
|
|
124
|
+
|
|
125
|
+
Keep `~/.config/analytics_ops/connection.json`, the Cloud project, and the
|
|
126
|
+
service account if Analytics Ops will remain in use. For a temporary-only
|
|
127
|
+
test, remove the connection file and revoke the downloaded key in Google
|
|
128
|
+
Cloud.
|
|
129
|
+
|
|
130
|
+
Keep the local path dependency until `0.2.0` is published. After publication,
|
|
131
|
+
replace it with:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
gem "analytics_ops", "~> 0.2", group: :development
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Run `bundle install`, then confirm `bundle info analytics_ops` resolves
|
|
138
|
+
version `0.2.0` from RubyGems.
|
|
139
|
+
|
|
140
|
+
## Release gate
|
|
141
|
+
|
|
142
|
+
Do not tag while any live read-only command or local release check is failing.
|
|
143
|
+
Require all of the following:
|
|
144
|
+
|
|
145
|
+
- The complete live smoke test passed.
|
|
146
|
+
- `bin/check`, package inspection, and `git diff --check` passed.
|
|
147
|
+
- Documentation and changelog describe the released behavior.
|
|
148
|
+
- The gem worktree is clean and the release commit is on `origin/master`.
|
|
149
|
+
- Version and changelog both say `0.2.0`.
|
|
150
|
+
|
|
151
|
+
Then create and push the annotated tag:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
git tag -a v0.2.0 -m "Release Analytics Ops 0.2.0"
|
|
155
|
+
git push origin v0.2.0
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The tag workflow verifies and publishes through RubyGems Trusted Publishing.
|
|
159
|
+
Never run a manual `gem push` or store a RubyGems API key.
|
data/docs/plan-format.md
CHANGED
|
@@ -63,6 +63,10 @@ before/after payloads, reversibility, and manual rollback guidance.
|
|
|
63
63
|
Resource names in update payloads must belong to the plan's property. Immutable
|
|
64
64
|
identity fields cannot change between `before` and `after`.
|
|
65
65
|
|
|
66
|
+
Currency custom-metric payloads include `restricted_metric_types` with
|
|
67
|
+
`cost_data`, `revenue_data`, or both. The classification is never inferred or
|
|
68
|
+
changed automatically.
|
|
69
|
+
|
|
66
70
|
## Findings are not operations
|
|
67
71
|
|
|
68
72
|
A finding may identify inaccessible resources, immutable conflicts, manual UI
|
data/docs/plan-schema-v1.json
CHANGED
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"resource_type": { "const": "data_stream" },
|
|
54
54
|
"resource_identity": { "pattern": "^stream:[0-9]{1,50}$" },
|
|
55
55
|
"operation": { "const": "update" },
|
|
56
|
-
"before": { "$ref": "#/$defs/
|
|
57
|
-
"after": { "$ref": "#/$defs/
|
|
56
|
+
"before": { "$ref": "#/$defs/dataStreamBefore" },
|
|
57
|
+
"after": { "$ref": "#/$defs/dataStreamAfter" }
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
{
|
|
@@ -124,17 +124,38 @@
|
|
|
124
124
|
"message": { "type": "string", "pattern": "^[^\\u0000-\\u001F\\u007F]{1,1000}$" }
|
|
125
125
|
}
|
|
126
126
|
},
|
|
127
|
-
"
|
|
127
|
+
"dataStreamBefore": {
|
|
128
128
|
"type": "object",
|
|
129
129
|
"additionalProperties": false,
|
|
130
130
|
"required": ["id", "name", "display_name", "type", "default_uri", "measurement_id"],
|
|
131
131
|
"properties": {
|
|
132
132
|
"id": { "$ref": "#/$defs/id" },
|
|
133
133
|
"name": { "$ref": "#/$defs/resourceName" },
|
|
134
|
-
"display_name": { "type": "string", "maxLength": 100 },
|
|
135
|
-
"type": { "
|
|
136
|
-
"default_uri": {
|
|
137
|
-
|
|
134
|
+
"display_name": { "type": "string", "maxLength": 100, "pattern": "^[^\\u0000-\\u001F\\u007F]*$" },
|
|
135
|
+
"type": { "const": "web" },
|
|
136
|
+
"default_uri": {
|
|
137
|
+
"type": ["string", "null"], "format": "uri", "pattern": "^[^\\u0000-\\u001F\\u007F]+$"
|
|
138
|
+
},
|
|
139
|
+
"measurement_id": {
|
|
140
|
+
"type": ["string", "null"], "maxLength": 64, "pattern": "^[^\\u0000-\\u001F\\u007F]+$"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"dataStreamAfter": {
|
|
145
|
+
"type": "object",
|
|
146
|
+
"additionalProperties": false,
|
|
147
|
+
"required": ["id", "name", "display_name", "type", "default_uri", "measurement_id"],
|
|
148
|
+
"properties": {
|
|
149
|
+
"id": { "$ref": "#/$defs/id" },
|
|
150
|
+
"name": { "$ref": "#/$defs/resourceName" },
|
|
151
|
+
"display_name": { "type": "string", "maxLength": 100, "pattern": "^[^\\u0000-\\u001F\\u007F]*$" },
|
|
152
|
+
"type": { "const": "web" },
|
|
153
|
+
"default_uri": {
|
|
154
|
+
"type": "string", "format": "uri", "pattern": "^[^\\u0000-\\u001F\\u007F]+$"
|
|
155
|
+
},
|
|
156
|
+
"measurement_id": {
|
|
157
|
+
"type": ["string", "null"], "maxLength": 64, "pattern": "^[^\\u0000-\\u001F\\u007F]+$"
|
|
158
|
+
}
|
|
138
159
|
}
|
|
139
160
|
},
|
|
140
161
|
"retention": {
|
|
@@ -144,13 +165,16 @@
|
|
|
144
165
|
"properties": {
|
|
145
166
|
"name": { "$ref": "#/$defs/resourceName" },
|
|
146
167
|
"event_data": { "$ref": "#/$defs/retentionValue" },
|
|
147
|
-
"user_data": { "$ref": "#/$defs/
|
|
168
|
+
"user_data": { "$ref": "#/$defs/userRetentionValue" },
|
|
148
169
|
"reset_on_new_activity": { "type": "boolean" }
|
|
149
170
|
}
|
|
150
171
|
},
|
|
151
172
|
"retentionValue": {
|
|
152
173
|
"enum": ["2_months", "14_months", "26_months", "38_months", "50_months"]
|
|
153
174
|
},
|
|
175
|
+
"userRetentionValue": {
|
|
176
|
+
"enum": ["2_months", "14_months"]
|
|
177
|
+
},
|
|
154
178
|
"keyEvent": {
|
|
155
179
|
"type": "object",
|
|
156
180
|
"additionalProperties": false,
|
|
@@ -166,11 +190,28 @@
|
|
|
166
190
|
"required": ["parameter_name", "display_name", "description", "scope", "disallow_ads_personalization"],
|
|
167
191
|
"properties": {
|
|
168
192
|
"parameter_name": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,39}$" },
|
|
169
|
-
"display_name": {
|
|
170
|
-
|
|
193
|
+
"display_name": {
|
|
194
|
+
"type": "string", "minLength": 1, "maxLength": 82,
|
|
195
|
+
"pattern": "^[^\\u0000-\\u001F\\u007F]+$"
|
|
196
|
+
},
|
|
197
|
+
"description": { "type": "string", "maxLength": 150, "pattern": "^[^\\u0000-\\u001F\\u007F]*$" },
|
|
171
198
|
"scope": { "enum": ["event", "user", "item"] },
|
|
172
199
|
"disallow_ads_personalization": { "type": "boolean" }
|
|
173
|
-
}
|
|
200
|
+
},
|
|
201
|
+
"allOf": [
|
|
202
|
+
{
|
|
203
|
+
"if": { "properties": { "scope": { "const": "user" } } },
|
|
204
|
+
"then": {
|
|
205
|
+
"properties": {
|
|
206
|
+
"parameter_name": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,23}$" }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"if": { "properties": { "scope": { "enum": ["event", "item"] } } },
|
|
212
|
+
"then": { "properties": { "disallow_ads_personalization": { "const": false } } }
|
|
213
|
+
}
|
|
214
|
+
]
|
|
174
215
|
},
|
|
175
216
|
"customDimensionUpdate": {
|
|
176
217
|
"type": "object",
|
|
@@ -179,46 +220,96 @@
|
|
|
179
220
|
"properties": {
|
|
180
221
|
"name": { "$ref": "#/$defs/resourceName" },
|
|
181
222
|
"parameter_name": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,39}$" },
|
|
182
|
-
"display_name": {
|
|
183
|
-
|
|
223
|
+
"display_name": {
|
|
224
|
+
"type": "string", "minLength": 1, "maxLength": 82,
|
|
225
|
+
"pattern": "^[^\\u0000-\\u001F\\u007F]+$"
|
|
226
|
+
},
|
|
227
|
+
"description": { "type": "string", "maxLength": 150, "pattern": "^[^\\u0000-\\u001F\\u007F]*$" },
|
|
184
228
|
"scope": { "enum": ["event", "user", "item"] },
|
|
185
229
|
"disallow_ads_personalization": { "type": "boolean" }
|
|
186
|
-
}
|
|
230
|
+
},
|
|
231
|
+
"allOf": [
|
|
232
|
+
{
|
|
233
|
+
"if": { "properties": { "scope": { "const": "user" } } },
|
|
234
|
+
"then": {
|
|
235
|
+
"properties": {
|
|
236
|
+
"parameter_name": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,23}$" }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"if": { "properties": { "scope": { "enum": ["event", "item"] } } },
|
|
242
|
+
"then": { "properties": { "disallow_ads_personalization": { "const": false } } }
|
|
243
|
+
}
|
|
244
|
+
]
|
|
187
245
|
},
|
|
188
246
|
"customMetricCreate": {
|
|
189
247
|
"type": "object",
|
|
190
248
|
"additionalProperties": false,
|
|
191
|
-
"required": [
|
|
249
|
+
"required": [
|
|
250
|
+
"parameter_name", "display_name", "description", "scope", "measurement_unit", "restricted_metric_types"
|
|
251
|
+
],
|
|
192
252
|
"properties": {
|
|
193
253
|
"parameter_name": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,39}$" },
|
|
194
|
-
"display_name": {
|
|
195
|
-
|
|
254
|
+
"display_name": {
|
|
255
|
+
"type": "string", "minLength": 1, "maxLength": 82,
|
|
256
|
+
"pattern": "^[^\\u0000-\\u001F\\u007F]+$"
|
|
257
|
+
},
|
|
258
|
+
"description": { "type": "string", "maxLength": 150, "pattern": "^[^\\u0000-\\u001F\\u007F]*$" },
|
|
196
259
|
"scope": { "const": "event" },
|
|
197
260
|
"measurement_unit": {
|
|
198
261
|
"enum": [
|
|
199
262
|
"standard", "currency", "feet", "meters", "kilometers", "miles",
|
|
200
263
|
"milliseconds", "seconds", "minutes", "hours"
|
|
201
264
|
]
|
|
265
|
+
},
|
|
266
|
+
"restricted_metric_types": {
|
|
267
|
+
"type": "array", "uniqueItems": true,
|
|
268
|
+
"items": { "enum": ["cost_data", "revenue_data"] }
|
|
202
269
|
}
|
|
203
|
-
}
|
|
270
|
+
},
|
|
271
|
+
"allOf": [
|
|
272
|
+
{
|
|
273
|
+
"if": { "properties": { "measurement_unit": { "const": "currency" } } },
|
|
274
|
+
"then": { "properties": { "restricted_metric_types": { "minItems": 1 } } },
|
|
275
|
+
"else": { "properties": { "restricted_metric_types": { "maxItems": 0 } } }
|
|
276
|
+
}
|
|
277
|
+
]
|
|
204
278
|
},
|
|
205
279
|
"customMetricUpdate": {
|
|
206
280
|
"type": "object",
|
|
207
281
|
"additionalProperties": false,
|
|
208
|
-
"required": [
|
|
282
|
+
"required": [
|
|
283
|
+
"name", "parameter_name", "display_name", "description", "scope", "measurement_unit",
|
|
284
|
+
"restricted_metric_types"
|
|
285
|
+
],
|
|
209
286
|
"properties": {
|
|
210
287
|
"name": { "$ref": "#/$defs/resourceName" },
|
|
211
288
|
"parameter_name": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,39}$" },
|
|
212
|
-
"display_name": {
|
|
213
|
-
|
|
289
|
+
"display_name": {
|
|
290
|
+
"type": "string", "minLength": 1, "maxLength": 82,
|
|
291
|
+
"pattern": "^[^\\u0000-\\u001F\\u007F]+$"
|
|
292
|
+
},
|
|
293
|
+
"description": { "type": "string", "maxLength": 150, "pattern": "^[^\\u0000-\\u001F\\u007F]*$" },
|
|
214
294
|
"scope": { "const": "event" },
|
|
215
295
|
"measurement_unit": {
|
|
216
296
|
"enum": [
|
|
217
297
|
"standard", "currency", "feet", "meters", "kilometers", "miles",
|
|
218
298
|
"milliseconds", "seconds", "minutes", "hours"
|
|
219
299
|
]
|
|
300
|
+
},
|
|
301
|
+
"restricted_metric_types": {
|
|
302
|
+
"type": "array", "uniqueItems": true,
|
|
303
|
+
"items": { "enum": ["cost_data", "revenue_data"] }
|
|
220
304
|
}
|
|
221
|
-
}
|
|
305
|
+
},
|
|
306
|
+
"allOf": [
|
|
307
|
+
{
|
|
308
|
+
"if": { "properties": { "measurement_unit": { "const": "currency" } } },
|
|
309
|
+
"then": { "properties": { "restricted_metric_types": { "minItems": 1 } } },
|
|
310
|
+
"else": { "properties": { "restricted_metric_types": { "maxItems": 0 } } }
|
|
311
|
+
}
|
|
312
|
+
]
|
|
222
313
|
}
|
|
223
314
|
}
|
|
224
315
|
}
|
data/docs/rails.md
CHANGED
|
@@ -14,6 +14,9 @@ Active Support.
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
bundle install
|
|
17
|
+
bundle exec analytics-ops setup \
|
|
18
|
+
--profile development \
|
|
19
|
+
--service-account /absolute/path/to/service-account.json
|
|
17
20
|
bin/rails generate analytics_ops:install
|
|
18
21
|
```
|
|
19
22
|
|
|
@@ -22,14 +25,20 @@ The generator creates only:
|
|
|
22
25
|
- `config/analytics_ops.yml`
|
|
23
26
|
- `bin/analytics-ops` with executable mode
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
If setup already created the configuration, the generator preserves it and
|
|
29
|
+
adds only the binstub. It never asks to overwrite the selected property.
|
|
30
|
+
|
|
31
|
+
The generated file has a minimal `development` profile and a fake-safe
|
|
32
|
+
property-ID placeholder. It contains no active stream, retention, key-event,
|
|
33
|
+
dimension, or metric policy, so installation alone cannot produce a mutation
|
|
34
|
+
plan. Add managed settings only after reviewing the configuration guide. Add
|
|
35
|
+
a profile matching another Rails environment, or set `ANALYTICS_OPS_PROFILE`.
|
|
28
36
|
|
|
29
37
|
## Rake tasks
|
|
30
38
|
|
|
31
39
|
```bash
|
|
32
40
|
bin/rake analytics:doctor
|
|
41
|
+
bin/rake analytics:overview
|
|
33
42
|
bin/rake analytics:audit
|
|
34
43
|
bin/rake analytics:plan
|
|
35
44
|
bin/rake analytics:verify
|
data/docs/reports.md
CHANGED
|
@@ -17,7 +17,11 @@ All standard recipes cover the previous 28 complete days
|
|
|
17
17
|
| `shares_and_prints` | Share and print events by calculator | `customEvent:calculator_slug` |
|
|
18
18
|
| `related_calculator_navigation` | Related-calculator clicks by source and destination | `customEvent:calculator_slug`, `customEvent:related_calculator_slug` |
|
|
19
19
|
| `commercial_outbound_clicks` | Commercial outbound clicks by calculator and destination | `customEvent:calculator_slug`, `customEvent:outbound_destination` |
|
|
20
|
-
| `realtime_events` | Event count
|
|
20
|
+
| `realtime_events` | Event count by event name | None |
|
|
21
|
+
|
|
22
|
+
The CLI also accepts `traffic` as an alias for `traffic_acquisition` and
|
|
23
|
+
`landing-pages` as an alias for `landing_pages`. Canonical names remain in
|
|
24
|
+
JSON and Ruby results.
|
|
21
25
|
|
|
22
26
|
The event names used by the calculator recipes are
|
|
23
27
|
`calculation_completed`, `result_shared`, `result_printed`,
|
|
@@ -27,15 +31,39 @@ If your site uses different names, create a custom Ruby definition.
|
|
|
27
31
|
## CLI
|
|
28
32
|
|
|
29
33
|
```bash
|
|
30
|
-
analytics-ops
|
|
31
|
-
analytics-ops report
|
|
32
|
-
analytics-ops report
|
|
34
|
+
analytics-ops overview
|
|
35
|
+
analytics-ops report traffic
|
|
36
|
+
analytics-ops report landing-pages --json
|
|
37
|
+
analytics-ops report calculator_completions --csv
|
|
33
38
|
analytics-ops realtime
|
|
34
39
|
```
|
|
35
40
|
|
|
36
41
|
CSV is accepted only for `report` and `realtime`. The CSV renderer protects
|
|
37
|
-
cells
|
|
38
|
-
|
|
42
|
+
headers and cells whose first meaningful character is `=`, `+`, `-`, or `@`,
|
|
43
|
+
including formula text hidden behind whitespace or control characters.
|
|
44
|
+
|
|
45
|
+
## Overview
|
|
46
|
+
|
|
47
|
+
`analytics-ops overview` uses one `batchRunReports` call containing five
|
|
48
|
+
small reports for the previous 28 complete days:
|
|
49
|
+
|
|
50
|
+
- totals for active users, sessions, and key events
|
|
51
|
+
- a daily trend
|
|
52
|
+
- traffic acquisition
|
|
53
|
+
- landing pages
|
|
54
|
+
- device categories
|
|
55
|
+
|
|
56
|
+
Each subreport has a small row limit, and Google property-quota information is
|
|
57
|
+
preserved when returned. Batching reduces network round trips; it does not
|
|
58
|
+
make the underlying report work quota-free. CSV is intentionally unavailable
|
|
59
|
+
for this multi-report result. Use `--json` for structured overview output.
|
|
60
|
+
|
|
61
|
+
The dimensionless totals request keeps empty rows so a brand-new property
|
|
62
|
+
still returns the expected metric headers.
|
|
63
|
+
|
|
64
|
+
The realtime recipe intentionally requests only `eventCount` with
|
|
65
|
+
`eventName`. Google does not allow `activeUsers` with that dimension. Realtime
|
|
66
|
+
is a short-lived event check rather than a complete user report.
|
|
39
67
|
|
|
40
68
|
## Ruby
|
|
41
69
|
|
|
@@ -49,6 +77,10 @@ result = workspace.report("traffic_acquisition")
|
|
|
49
77
|
puts result.headers
|
|
50
78
|
puts result.rows
|
|
51
79
|
puts result.metadata
|
|
80
|
+
|
|
81
|
+
overview = workspace.overview
|
|
82
|
+
puts overview.report("overview_totals").rows
|
|
83
|
+
puts overview.property_quota
|
|
52
84
|
```
|
|
53
85
|
|
|
54
86
|
Custom immutable definition:
|
|
@@ -82,6 +114,12 @@ metrics, invalid date order, filters that reference unselected fields,
|
|
|
82
114
|
non-finite numeric filters, invalid ordering, and unbounded limits. Realtime
|
|
83
115
|
definitions cannot use date ranges or offsets.
|
|
84
116
|
|
|
117
|
+
Dates use only `YYYY-MM-DD`, `NdaysAgo`, `yesterday`, or `today`. Absolute and
|
|
118
|
+
relative ranges are checked for reversed endpoints. A request may contain up
|
|
119
|
+
to four uniquely named ranges; comparison responses include Google's
|
|
120
|
+
automatic `dateRange` dimension. Numeric filters accept only finite doubles or
|
|
121
|
+
signed 64-bit integers.
|
|
122
|
+
|
|
85
123
|
## Result shape
|
|
86
124
|
|
|
87
125
|
`result.to_h` contains:
|
data/docs/safety.md
CHANGED
|
@@ -4,9 +4,12 @@ Analytics Ops is read-only unless you explicitly apply a saved plan.
|
|
|
4
4
|
|
|
5
5
|
## What cannot mutate
|
|
6
6
|
|
|
7
|
-
`doctor`, `discover`, `snapshot`, `audit`, `plan`, `verify`,
|
|
8
|
-
`report`, and `realtime` only read Google APIs.
|
|
9
|
-
|
|
7
|
+
`properties`, `doctor`, `discover`, `snapshot`, `audit`, `plan`, `verify`,
|
|
8
|
+
`overview`, `report`, and `realtime` only read Google Analytics APIs. Setup
|
|
9
|
+
validates a service-account key, verifies access, remembers only its path, and
|
|
10
|
+
creates local configuration; it never changes Analytics. Loading the gem,
|
|
11
|
+
constructing a connection, parsing YAML, and booting Rails make no network
|
|
12
|
+
request.
|
|
10
13
|
|
|
11
14
|
## What apply requires
|
|
12
15
|
|
|
@@ -20,6 +23,9 @@ An apply needs all of these:
|
|
|
20
23
|
The applier executes only the operations stored in the file. It never replans
|
|
21
24
|
during apply and never turns findings into changes.
|
|
22
25
|
|
|
26
|
+
The Ruby API also requires the literal boolean `true`; truthy strings or
|
|
27
|
+
objects do not count as confirmation.
|
|
28
|
+
|
|
23
29
|
## Stale plans
|
|
24
30
|
|
|
25
31
|
Every plan fingerprints normalized managed remote state with SHA-256. If that
|
|
@@ -74,7 +80,7 @@ with mode 0600. Loading rejects:
|
|
|
74
80
|
- unknown resource payload fields
|
|
75
81
|
- unsupported operations
|
|
76
82
|
- no-op updates and immutable-field changes
|
|
77
|
-
- secret-shaped fields and control characters
|
|
83
|
+
- secret-shaped fields, credential-shaped text, and control characters
|
|
78
84
|
- resource names for another property
|
|
79
85
|
|
|
80
86
|
Workspace and adapter checks provide additional cross-property protection.
|
|
@@ -83,11 +89,17 @@ The public schema is [plan-schema-v1.json](plan-schema-v1.json).
|
|
|
83
89
|
## Credential and data boundaries
|
|
84
90
|
|
|
85
91
|
- Credentials never belong in YAML, plans, fixtures, logs, or command output.
|
|
92
|
+
- `~/.config/analytics_ops/connection.json` contains only a key path, is
|
|
93
|
+
atomic, and uses mode `0600`; the key is never copied.
|
|
94
|
+
- Ambient Application Default Credentials and API keys are never used.
|
|
95
|
+
- Read-only commands request only `analytics.readonly`; guarded apply also
|
|
96
|
+
requests `analytics.edit`.
|
|
86
97
|
- Report rows are never logged automatically.
|
|
87
98
|
- The gem has no telemetry and stores no report results.
|
|
88
99
|
- The gem does not inject browser analytics or manage consent.
|
|
89
100
|
- Production mutation credentials should not exist in a Rails web container.
|
|
90
101
|
- Real production IDs and exports do not belong in repository fixtures.
|
|
91
102
|
|
|
92
|
-
Use a
|
|
93
|
-
|
|
103
|
+
Use a Viewer service account for reports-only installations. If one service
|
|
104
|
+
account has the Editor role, Analytics Ops still requests edit scope only
|
|
105
|
+
inside the reviewed apply path.
|
data/docs/troubleshooting.md
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
Start with:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
analytics-ops
|
|
6
|
+
analytics-ops setup --service-account /absolute/path/to/service-account.json
|
|
7
|
+
analytics-ops properties
|
|
8
|
+
analytics-ops doctor --json
|
|
7
9
|
```
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
`properties` works without configuration. Setup verifies both APIs and creates
|
|
12
|
+
the minimal file; doctor performs the complete configured-property check.
|
|
10
13
|
|
|
11
14
|
## Configuration error
|
|
12
15
|
|
|
@@ -23,28 +26,51 @@ stream_id: "987654321"
|
|
|
23
26
|
|
|
24
27
|
Do not use account ID `100000001` or measurement ID `G-EXAMPLE1` in those
|
|
25
28
|
fields. Unknown keys, ERB, YAML aliases, missing environment variables, and
|
|
26
|
-
secret-shaped
|
|
29
|
+
secret- or credential-shaped material are intentionally rejected.
|
|
30
|
+
|
|
31
|
+
Currency custom metrics require an explicit restricted-data classification:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
measurement_unit: currency
|
|
35
|
+
restricted_metric_types: [revenue_data]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use `cost_data`, `revenue_data`, or both according to the metric's meaning.
|
|
39
|
+
Analytics Ops will not guess. User-data retention accepts only `2_months` or
|
|
40
|
+
`14_months`; longer 360 periods apply only to event data.
|
|
27
41
|
|
|
28
42
|
## Authentication failure (exit 66)
|
|
29
43
|
|
|
30
|
-
|
|
44
|
+
Analytics Ops could not load or use the configured service account.
|
|
31
45
|
|
|
32
46
|
```bash
|
|
33
|
-
|
|
34
|
-
--
|
|
47
|
+
analytics-ops setup \
|
|
48
|
+
--service-account /absolute/path/outside/repositories/service-account.json
|
|
35
49
|
```
|
|
36
50
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
Check that:
|
|
52
|
+
|
|
53
|
+
- the file still exists at that path
|
|
54
|
+
- it is a JSON key whose `type` is `service_account`
|
|
55
|
+
- the key has not been revoked in Google Cloud
|
|
56
|
+
- the Analytics Admin and Data APIs are enabled in its Cloud project
|
|
57
|
+
- the service-account email is added in GA4 Access Management
|
|
58
|
+
|
|
59
|
+
The saved pointer is `~/.config/analytics_ops/connection.json`. It contains
|
|
60
|
+
only the key path. Rerunning setup with `--service-account` safely replaces
|
|
61
|
+
that pointer after API verification succeeds.
|
|
62
|
+
|
|
63
|
+
Analytics Ops does not fall back to `gcloud`, browser login, Application
|
|
64
|
+
Default Credentials, `GOOGLE_APPLICATION_CREDENTIALS`, or API keys.
|
|
40
65
|
|
|
41
66
|
## Permission failure (exit 77)
|
|
42
67
|
|
|
43
|
-
The
|
|
44
|
-
not enough. Add
|
|
45
|
-
|
|
68
|
+
The service-account identity needs access inside the GA4 property. Cloud IAM
|
|
69
|
+
alone is not enough. Add its email to GA4 Access Management with Viewer for
|
|
70
|
+
reads or Editor for plan application.
|
|
46
71
|
|
|
47
|
-
Use `analytics-ops
|
|
72
|
+
Use `analytics-ops properties` to see accessible numeric property IDs without
|
|
73
|
+
creating configuration. `discover` additionally retrieves streams.
|
|
48
74
|
|
|
49
75
|
## API or remote failure (exit 69)
|
|
50
76
|
|
|
@@ -53,6 +79,9 @@ credential's Cloud project. Check the configuration ID and the error's typed
|
|
|
53
79
|
message. Invalid dimensions, metrics, custom definitions, or property
|
|
54
80
|
restrictions can also produce this status.
|
|
55
81
|
|
|
82
|
+
When setup recognizes disabled APIs, enable both APIs in the Google Cloud
|
|
83
|
+
project that owns the service account.
|
|
84
|
+
|
|
56
85
|
## Quota (exit 75) or timeout (exit 74)
|
|
57
86
|
|
|
58
87
|
Wait for Google quota to recover, reduce report frequency, or use a positive
|
|
@@ -15,7 +15,8 @@ module AnalyticsOps
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def call(plan, confirm: false)
|
|
18
|
-
raise ConfirmationRequiredError, "Applying a plan requires explicit confirmation" unless confirm
|
|
18
|
+
raise ConfirmationRequiredError, "Applying a plan requires explicit boolean confirmation" unless confirm == true
|
|
19
|
+
raise InvalidPlanError, "plan must be an AnalyticsOps::Plan" unless plan.is_a?(Plan)
|
|
19
20
|
|
|
20
21
|
current = @admin.snapshot(plan.property_id)
|
|
21
22
|
unless current.fingerprint == plan.snapshot_fingerprint
|