@crvouga/mockingbird-service-google-ads 1.2.0 → 1.3.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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # Changelog — @crvouga/mockingbird-service-google-ads
2
2
 
3
- ## 1.2.0 (2026-10-11)
3
+ ## 1.3.0 (2026-10-11)
4
+
5
+ ### Features
6
+
7
+ - resolve reported emulator coverage gaps ([6edc643](https://github.com/crvouga/mockingbird/commit/6edc6438444853a51aca27849c4c8c0449109ea0))
8
+
9
+ ## 1.2.0 (2026-10-08)
4
10
 
5
11
  ### Features
6
12
 
package/README.md CHANGED
@@ -51,7 +51,7 @@ faults, and resets are isolated by namespace.
51
51
 
52
52
  | Operation | Route | Behavior |
53
53
  | --- | --- | --- |
54
- | SearchGoogleAds | `POST /v25/customers/{customerId}/googleAds:search` | GAQL selection, filtering, ordering, aggregation, summary rows, stable 10,000-row pages and validate-only requests |
54
+ | SearchGoogleAds | `POST /v25/customers/{customerId}/googleAds:search` | GAQL selection, filtering, ordering, aggregation, summary rows, fixed 10,000-row pages, two-hour page tokens bound to the query, and validate-only requests |
55
55
  | SearchStreamGoogleAds | `POST /v25/customers/{customerId}/googleAds:searchStream` | Deterministic JSON batches and optional summary rows |
56
56
  | MutateCampaignBudgets | `POST /v25/customers/{customerId}/campaignBudgets:mutate` | Create, update, remove, update masks, validate-only and atomic or partial failure |
57
57
  | UploadClickConversions | `POST /v25/customers/{customerId}:uploadClickConversions` | Click identifiers, conversion actions, duplicate detection, job IDs and partial failure |
@@ -63,6 +63,50 @@ faults, and resets are isolated by namespace.
63
63
  The exact request and response schemas are in [`openapi.yaml`](openapi.yaml), and the generated
64
64
  support matrix is in [`SUPPORT.md`](SUPPORT.md).
65
65
 
66
+ Campaign budgets are read back with GAQL (`FROM campaign_budget`); uploaded conversions and
67
+ collected events are read through the inspection routes below, and events also through the
68
+ Analytics Data reports. REST rows use protobuf JSON names (`costMicros`, `conversionsValue`,
69
+ `conversionActionCategory`) and int64 values are strings; GAQL field names stay snake_case.
70
+
71
+ Google Ads failures are a `google.rpc.Status` whose `details` hold a `GoogleAdsFailure` with the
72
+ per-operation `errorCode`, `message`, `location` and a `requestId` equal to the `request-id`
73
+ response header. Unparsable JSON and every Analytics Data failure are a bare status with no Ads
74
+ detail and no `request-id` header.
75
+
76
+ ## Where Google differs from a common assumption
77
+
78
+ These follow Google's documentation and the pinned `googleapis` v25 protos rather than what a
79
+ client written against an older contract may expect.
80
+
81
+ - **Search page size is fixed.** `Search` returns pages of up to 10,000 rows and a request that
82
+ sets `pageSize` is rejected with `requestError: PAGE_SIZE_NOT_SUPPORTED`. Continue by re-sending
83
+ the identical query with `pageToken`; a changed query is `INVALID_PAGE_TOKEN` and a token unused
84
+ for two hours is `EXPIRED_PAGE_TOKEN`
85
+ ([paging guide](https://developers.google.com/google-ads/api/docs/reporting/paging),
86
+ [`SearchGoogleAdsRequest`](https://github.com/googleapis/googleapis/blob/03a91044136a014466d4293eb1fe91f2b02075d2/google/ads/googleads/v25/services/google_ads_service.proto)).
87
+ To page a small fixture, lower the emulator's page with the `searchPageSize` setting; the
88
+ request still cannot choose it.
89
+ - **`/mp/collect` never reports validation errors over HTTP.** It answers an empty `204` for a
90
+ bad secret, an unparsable body or a rejected event alike. Inspect a rejection with
91
+ `POST /debug/mp/collect`, which returns `validationMessages` and stores nothing, or with
92
+ `GET /__admin/request-metadata`, which records `accepted`, `rejected`, `duplicate` and `reasons`
93
+ per request
94
+ ([validation guide](https://developers.google.com/analytics/devguides/collection/protocol/ga4/validating-events)).
95
+ - **Timestamps older than 72 hours are overridden, not rejected,** unless the request sets
96
+ `validation_behavior: "ENFORCE_RECOMMENDATIONS"`
97
+ ([sending guide](https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag)).
98
+ Google documents no bound for future timestamps, so the one-minute `futureToleranceMs` default
99
+ is a test policy of this emulator, not a vendor rule; set it to `null` to disable it.
100
+ - **A validate-only budget mutation returns errors only.** A valid preflight answers `{}` with no
101
+ `results` and writes nothing
102
+ ([`MutateCampaignBudgetsRequest`](https://github.com/googleapis/googleapis/blob/03a91044136a014466d4293eb1fe91f2b02075d2/google/ads/googleads/v25/services/campaign_budget_service.proto)).
103
+ - **Mutations take no idempotency key.** The duplicate guards are Google's own: a shared budget
104
+ name (`campaignBudgetError: DUPLICATE_NAME`), a conversion `orderId`
105
+ (`ORDER_ID_ALREADY_IN_USE`), a click and conversion time (`CLICK_CONVERSION_ALREADY_EXISTS`),
106
+ and a web `purchase` event's `transaction_id` per user.
107
+ - **`developer-token` is accepted and ignored.** The pinned protos mark the developer-token
108
+ errors as sunset; `login-customer-id` is still checked against the customer's manager.
109
+
66
110
  ## Controls and failures
67
111
 
68
112
  Service-specific controls are protected by the shared `x-mockingbird-admin-key` guard and live
@@ -70,21 +114,41 @@ beside the shared health, state, clock, journal, fault, reset, and Timeline endp
70
114
 
71
115
  | Control | Purpose |
72
116
  | --- | --- |
73
- | `GET /__admin/events` | Inspect stored Analytics events |
74
- | `GET /__admin/conversions` | Inspect uploaded conversions |
117
+ | `GET /__admin/events` | Inspect stored Analytics events, oldest first |
118
+ | `GET /__admin/conversions` | Inspect uploaded conversions, oldest first |
75
119
  | `GET /__admin/budgets` | Inspect campaign budgets |
76
- | `GET /__admin/budget-mutations` | Inspect the mutation audit trail |
120
+ | `GET /__admin/budget-mutations` | Inspect the mutation audit trail: request ID, `before` and `after` per write |
77
121
  | `GET /__admin/request-metadata` | Inspect redacted request metadata |
78
- | `POST /__admin/daily-metrics` | Seed synthetic daily Ads metrics |
122
+ | `POST /__admin/daily-metrics` | Seed synthetic daily Ads metrics (`availableAt` delays one row) |
79
123
  | `POST /__admin/page-tokens/expire` | Expire all current GAQL page tokens |
80
- | `GET /__admin/settings` | Read reporting lag, page-token TTL and future-timestamp policy |
124
+ | `GET /__admin/settings` | Read `reportingLagMs`, `pageTokenTtlMs`, `futureToleranceMs` and `searchPageSize` |
81
125
  | `PUT /__admin/settings` | Update those deterministic test settings |
82
126
 
83
- Fault presets include `quota_exhausted`, `server_error`, `network_reset`, `slow_response`,
84
- `ambiguous_budget_write`, `partial_budget_failure`, and `partial_conversion_failure`. The
85
- ambiguous-write preset commits the mutation before returning 503 so retry logic can be tested
86
- against an uncertain outcome. Sensitive event and conversion payloads are sealed in state;
87
- journals and inspection metadata contain only the fields needed for assertions.
127
+ | To | Use |
128
+ | --- | --- |
129
+ | Advance the reporting date | `POST /__admin/clock {"advance": "1d"}` or `{"set": "<ISO-8601>"}`; GAQL `DURING` ranges and Analytics `today`/`yesterday`/`NdaysAgo` follow it |
130
+ | Delay reporting | `PUT /__admin/settings {"reportingLagMs": n}` hides new events until the clock passes it |
131
+ | Page a small fixture | `PUT /__admin/settings {"searchPageSize": n}` (1 to 10,000) |
132
+ | Add latency | the `slow_response` preset, or `POST /__admin/faults {"latencyMs": n, "count": 1}` |
133
+ | Fail one request | the `server_error` or `network_reset` preset |
134
+ | Throttle | the `quota_exhausted` or `rate_limited` preset; `"count": n` throttles n requests |
135
+ | See what arrived | `GET /__admin/requests` (the journal) and `GET /__admin/request-metadata` |
136
+
137
+ Fault presets are `quota_exhausted`, `rate_limited`, `server_error`, `network_reset`,
138
+ `slow_response`, `ambiguous_budget_write`, `partial_budget_failure`, and
139
+ `partial_conversion_failure`. Each fires once unless `count` says otherwise, and is scoped to the
140
+ calling namespace. The two throttling presets answer `429 RESOURCE_EXHAUSTED` with
141
+ `quotaError: RESOURCE_EXHAUSTED` or `RESOURCE_TEMPORARILY_EXHAUSTED` and the backoff in
142
+ `details.quotaErrorDetails.retryDelay`, before anything is written.
143
+
144
+ `ambiguous_budget_write` commits the next executed budget mutation and then returns `503`, so
145
+ retry logic can be tested against an uncertain outcome. A validate-only preflight executes
146
+ nothing and does not spend it: the fault waits for the real call that follows. Reconcile before
147
+ replaying: the `503` carries a `request-id` header, and `GET /__admin/budget-mutations` lists the
148
+ write under that request ID with the budget's `before` and `after`.
149
+
150
+ Sensitive event and conversion payloads are sealed in state; journals and inspection metadata
151
+ contain only the fields needed for assertions.
88
152
 
89
153
  ## API
90
154
 
@@ -100,11 +164,19 @@ and the public fixture/runtime option types. The Node `./server` entry exports `
100
164
  Acceptance tests cover GAQL precision and paging, budget preflight and partial writes, conversion
101
165
  deduplication, GA4 event validation and reporting, OAuth scope/expiry behavior, encrypted state,
102
166
  namespaces, resets, fault recovery, and the unmodified `google-auth-library` 11.1.0 HTTP signing
103
- path. Property tests exercise every parity-enabled operation and prove that a divergent transport
104
- is detected deterministically.
105
-
106
- The emulator intentionally implements a bounded subset. It does not model the complete Google Ads
107
- resource graph, every GAQL function, every Analytics dimension or metric, real Google identity,
108
- production quota allocation, billing, dashboards, attribution processing, or undocumented
109
- backend behavior. Normal operation is local and does not contact Google; only the explicit parity
110
- command uses credentials supplied through `.env.local` or GitHub Actions secrets.
167
+ path. Runtime tests drive every control above over `/__admin`. Property tests exercise every
168
+ parity-enabled operation and prove that a divergent transport is detected deterministically.
169
+
170
+ `test/consumer.ts` is a port of the raw-fetch traffic described in the original service request.
171
+ It is not the requesting application's client, whose source is not in this repository, so no test
172
+ here runs that client unmodified.
173
+
174
+ ## Deliberately not modelled
175
+
176
+ The emulator implements a bounded subset. It does not model the complete Google Ads resource
177
+ graph, every GAQL function, GAQL field-compatibility rules between segments and metrics, every
178
+ Analytics dimension or metric, real Google identity, production quota allocation, billing,
179
+ dashboards, attribution processing, or undocumented backend behavior. Uploaded conversions do not
180
+ feed `metrics.conversions`; seed those with daily metrics. Normal operation is local and does not
181
+ contact Google; only the explicit parity command uses credentials supplied through `.env.local`
182
+ or GitHub Actions secrets.
@@ -3,7 +3,7 @@ import {
3
3
  createRuntime,
4
4
  parseDuration,
5
5
  resolveAdminPrefix
6
- } from "./chunk-DDJPMCRI.js";
6
+ } from "./chunk-NU66LXUZ.js";
7
7
 
8
8
  // ../../adapters/node/dist/cli.js
9
9
  import { readFile as readFile2 } from "node:fs/promises";
@@ -817,4 +817,4 @@ export {
817
817
  createServer2 as createServer,
818
818
  serveTarget
819
819
  };
820
- //# sourceMappingURL=chunk-KCDQPGFS.js.map
820
+ //# sourceMappingURL=chunk-5T6QIJK4.js.map