@admoai/sdk-scenarios 0.0.0-stage → 1.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.
- package/CHANGELOG.md +19 -0
- package/README.md +29 -2
- package/package.json +15 -4
- package/scenarios.feature +371 -0
- package/scenarios.json +946 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Versioning: a scenario added or an `expect` tightened is a **minor** release; a scenario
|
|
4
|
+
removed, or a `request`/`expect` key renamed (every SDK's interpreter must change), is a
|
|
5
|
+
**major** one. Each SDK vendors one version and its own CI checks the vendored bytes against it.
|
|
6
|
+
|
|
7
|
+
## 1.1.0 — 2026-10-21
|
|
8
|
+
|
|
9
|
+
Also ships `scenarios.feature`: the same 39 scenarios rendered as Gherkin for people to read
|
|
10
|
+
(admoai/adhub#4348). It is generated from `scenarios.json` by `tools/sdkfeaturegen` in adhub and
|
|
11
|
+
is never edited by hand; the SDK runners keep executing the JSON, which is byte-identical to
|
|
12
|
+
1.0.0's.
|
|
13
|
+
|
|
14
|
+
## 1.0.0 — 2026-10-20
|
|
15
|
+
|
|
16
|
+
The 39 scenarios the three SDKs already carried, byte for byte (sha256 `a55f68eb…`):
|
|
17
|
+
L1 L5 L6 L7 L8 · M1 M2 M3 M5 M6 M7 M8 · P1 P2 P3 P6 P8 · V1 V3 V4 V5 V6 V9 · Z1 Z2 · SP1–SP14.
|
|
18
|
+
First release from adhub, the source of truth since admoai/adhub#4158; before it the file was
|
|
19
|
+
mirrored into each SDK repo and held equal by a hash in each and a parity check in adhub.
|
package/README.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @admoai/sdk-scenarios
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The scenarios every AdMoai mobile SDK (iOS, Android, Flutter) must pass against a real
|
|
4
|
+
decision-engine: given a request, the outcome a publisher's app observes. One definition,
|
|
5
|
+
executed by a thin interpreter in each SDK, so the three cannot make different claims.
|
|
6
|
+
|
|
7
|
+
This package is **data only**: `scenarios.json` (the manifest, `schemaVersion` inside) and
|
|
8
|
+
`scenarios.feature`, the same scenarios rendered as Gherkin for a PM or an integrating
|
|
9
|
+
publisher to read. The JSON is what the SDK runners execute; the feature is generated from it.
|
|
10
|
+
Neither carries URLs, hosts, credentials or tenant identifiers — `check.sh` refuses a release
|
|
11
|
+
whose manifest does.
|
|
12
|
+
|
|
13
|
+
## For an SDK
|
|
14
|
+
|
|
15
|
+
Vendor one version and pin it; your CI verifies the vendored bytes against the published
|
|
16
|
+
tarball, and your tests read the vendored file (they never touch the network):
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
curl -sSfL "https://registry.npmjs.org/@admoai/sdk-scenarios/-/sdk-scenarios-$VERSION.tgz" \
|
|
20
|
+
| tar -xzO package/scenarios.json | cmp - path/to/vendored/scenarios.json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## For adhub (the source)
|
|
24
|
+
|
|
25
|
+
The source of truth is `sdk/scenarios/` in admoai/adhub. To change a scenario: edit
|
|
26
|
+
`scenarios.json`, regenerate the Gherkin with `go run ./tools/sdkfeaturegen` from the
|
|
27
|
+
repository root, bump `version` in `package.json` (minor: added or tightened; major: removed
|
|
28
|
+
or a key renamed), add a `CHANGELOG.md` entry, and run `sdk/scenarios/check.sh`. Merging to
|
|
29
|
+
`main` publishes the new version (`.github/workflows/sdk-scenarios-release.yml`); each SDK then
|
|
30
|
+
moves to it in its own PR. See `docs/guides/sdk-ci.md`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@admoai/sdk-scenarios",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "The cross-SDK decision & tracking scenarios every AdMoai SDK asserts against a real decision-engine. Data only.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"scenarios.json",
|
|
8
|
+
"scenarios.feature",
|
|
9
|
+
"CHANGELOG.md",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"admoai",
|
|
14
|
+
"sdk",
|
|
15
|
+
"contract-tests"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# GENERATED from scenarios.json — do not edit; regenerate with: go run ./tools/sdkfeaturegen
|
|
2
|
+
#
|
|
3
|
+
# scenarios.json is what the iOS, Android and Flutter manifest runners execute against a
|
|
4
|
+
# real decision-engine. This is its readable rendering: what every AdMoai SDK promises a
|
|
5
|
+
# publisher's app, in the same words for all three. CI regenerates it and fails on any
|
|
6
|
+
# difference, so it cannot drift from what the runners execute.
|
|
7
|
+
# How it is published and kept fresh: docs/guides/sdk-ci.md, Section 7.
|
|
8
|
+
# Manifest schemaVersion: 1
|
|
9
|
+
|
|
10
|
+
Feature: Cross-SDK decision & tracking contract
|
|
11
|
+
Shared cross-SDK E2E scenario manifest. ONE definition, executed by a thin interpreter in each
|
|
12
|
+
SDK, so the three suites cannot drift. Declarative only: given a request, expect an outcome.
|
|
13
|
+
Sponsored Pin tracking scenarios add `pinTracking`: the point-tracking calls an app makes on
|
|
14
|
+
the served pins, as data, and the beacons that must reach the engine. Procedural scenarios
|
|
15
|
+
(log capture, concurrency, retry, TTL waits) stay hand-written per SDK. Mirrored
|
|
16
|
+
byte-identically into all three repos; a checksum test fails the build if the copies diverge.
|
|
17
|
+
Every expected value here was verified against a live local decision-engine before being
|
|
18
|
+
written.
|
|
19
|
+
|
|
20
|
+
@L @native
|
|
21
|
+
Scenario: L1 — a normal native ad serves with complete, well-formed metadata
|
|
22
|
+
Given the app requests placement "home"
|
|
23
|
+
Then an ad is served
|
|
24
|
+
And exactly 1 decision is returned
|
|
25
|
+
And the creative carries complete metadata: ad, creative, placement and template ids
|
|
26
|
+
And the creative's priority is one of standard, sponsorship, house
|
|
27
|
+
And the creative is not a Journey Ad
|
|
28
|
+
And the creative's format is "native"
|
|
29
|
+
|
|
30
|
+
@L @native
|
|
31
|
+
Scenario: L5 — a placement with no eligible ad returns a clean no-ad, not an error
|
|
32
|
+
Given the app requests placement "postroll"
|
|
33
|
+
Then no ad is served, and the placement answers with a clean no-fill
|
|
34
|
+
And exactly 1 decision is returned
|
|
35
|
+
|
|
36
|
+
@L @native
|
|
37
|
+
Scenario: L6 — the advertiser block is complete (name, legalName, logoUrl)
|
|
38
|
+
Given the app requests placement "home"
|
|
39
|
+
Then an ad is served
|
|
40
|
+
And the creative names its advertiser: name, legal name and logo
|
|
41
|
+
|
|
42
|
+
@L @native
|
|
43
|
+
Scenario: L7 — the template block carries a non-empty key
|
|
44
|
+
Given the app requests placement "home"
|
|
45
|
+
Then an ad is served
|
|
46
|
+
And the creative names its template
|
|
47
|
+
|
|
48
|
+
@L @native
|
|
49
|
+
Scenario: L8 — a served native ad exposes an impression URL, a click URL and non-empty contents
|
|
50
|
+
Given the app requests placement "home"
|
|
51
|
+
Then an ad is served
|
|
52
|
+
And the creative carries its content fields
|
|
53
|
+
And the creative exposes an impression URL
|
|
54
|
+
And the creative exposes at least 1 click URL
|
|
55
|
+
|
|
56
|
+
@M
|
|
57
|
+
Scenario: M1 — count=2 returns at most two creatives, and they are distinct
|
|
58
|
+
Given the app requests placement "promotions"
|
|
59
|
+
And placement "promotions" asks for 2 creatives
|
|
60
|
+
Then an ad is served
|
|
61
|
+
And exactly 1 decision is returned
|
|
62
|
+
And at most 2 creatives are returned
|
|
63
|
+
And the returned creatives are all different
|
|
64
|
+
|
|
65
|
+
@M
|
|
66
|
+
Scenario: M2 — three placements in one request each get their own correctly-keyed decision
|
|
67
|
+
Given the app requests placement "home"
|
|
68
|
+
And the app requests placement "promotions"
|
|
69
|
+
And the app requests placement "search"
|
|
70
|
+
Then an ad is served
|
|
71
|
+
And exactly 3 decisions are returned
|
|
72
|
+
And every requested placement comes back with its own decision
|
|
73
|
+
|
|
74
|
+
@M
|
|
75
|
+
Scenario: M3 — an advertiserId filter still serves that advertiser's ad
|
|
76
|
+
Given the app requests placement "promotions"
|
|
77
|
+
And placement "promotions" is restricted to advertiser "advertiser_01JEPGMJXARVYSJA0CAM9FQ79Y"
|
|
78
|
+
Then an ad is served
|
|
79
|
+
And the creative names its advertiser: name, legal name and logo
|
|
80
|
+
|
|
81
|
+
@M
|
|
82
|
+
Scenario: M5 — format=native on a mixed placement returns a native ad
|
|
83
|
+
Given the app requests placement "promotions"
|
|
84
|
+
And placement "promotions" asks for format "native"
|
|
85
|
+
Then an ad is served
|
|
86
|
+
And the creative's format is "native"
|
|
87
|
+
|
|
88
|
+
@M
|
|
89
|
+
Scenario: M6 — format=video on the same mixed placement returns a video ad instead
|
|
90
|
+
Given the app requests placement "promotions"
|
|
91
|
+
And placement "promotions" asks for format "video"
|
|
92
|
+
Then an ad is served
|
|
93
|
+
And the creative's format is "video"
|
|
94
|
+
|
|
95
|
+
@M
|
|
96
|
+
Scenario: M7 — omitting format still serves — the engine chooses
|
|
97
|
+
Given the app requests placement "promotions"
|
|
98
|
+
Then an ad is served
|
|
99
|
+
|
|
100
|
+
@M
|
|
101
|
+
Scenario: M8 — a journey-owned placement and a normal placement resolve independently in ONE request
|
|
102
|
+
Given the app requests placement "promotions"
|
|
103
|
+
And the app requests placement "home"
|
|
104
|
+
And the request carries a fresh session id
|
|
105
|
+
And the user has opted in to Journey Ads
|
|
106
|
+
Then an ad is served
|
|
107
|
+
And exactly 2 decisions are returned
|
|
108
|
+
|
|
109
|
+
@P @video
|
|
110
|
+
Scenario: P1 — a non-skippable JSON video exposes json delivery and five video events (no skip)
|
|
111
|
+
Given the app requests placement "json_none"
|
|
112
|
+
Then an ad is served
|
|
113
|
+
And the creative's format is "video"
|
|
114
|
+
And the creative is delivered as "json"
|
|
115
|
+
And the creative exposes exactly 5 video event URLs
|
|
116
|
+
And the video events include start, first_quartile, midpoint, third_quartile, complete
|
|
117
|
+
|
|
118
|
+
@P @video
|
|
119
|
+
Scenario: P2 — a vast_tag ad exposes its tag URL and surfaces NO engine-side beacons (no double count)
|
|
120
|
+
Given the app requests placement "vasttag_none"
|
|
121
|
+
Then an ad is served
|
|
122
|
+
And the creative is delivered as "vast_tag"
|
|
123
|
+
And the creative exposes no engine impression URL, because the VAST owns the impression
|
|
124
|
+
And the creative exposes exactly 0 video event URLs
|
|
125
|
+
And the creative exposes a VAST tag URL
|
|
126
|
+
|
|
127
|
+
@P @video
|
|
128
|
+
Scenario: P3 — a vast_xml ad exposes base64 that decodes to a VAST document, and no beacons
|
|
129
|
+
Given the app requests placement "vastxml_none"
|
|
130
|
+
Then an ad is served
|
|
131
|
+
And the creative is delivered as "vast_xml"
|
|
132
|
+
And the creative exposes no engine impression URL, because the VAST owns the impression
|
|
133
|
+
And the creative exposes exactly 0 video event URLs
|
|
134
|
+
And the creative's inline VAST XML decodes to a VAST document
|
|
135
|
+
|
|
136
|
+
@P @video
|
|
137
|
+
Scenario: P6 — a skippable video resolves skippability from ENGINE METADATA, plus a sixth skip event
|
|
138
|
+
Given the app requests placement "json_none_skippable"
|
|
139
|
+
Then an ad is served
|
|
140
|
+
And the creative exposes exactly 6 video event URLs
|
|
141
|
+
And the metadata says the video is skippable
|
|
142
|
+
And the metadata's skip offset is 5 seconds
|
|
143
|
+
And the SDK's skippable helper says the video is skippable
|
|
144
|
+
And the SDK's skip-offset helper returns "5"
|
|
145
|
+
|
|
146
|
+
@P @video
|
|
147
|
+
Scenario: P8 — an end-card video reports its configured endCardMode
|
|
148
|
+
Given the app requests placement "json_native_endcard"
|
|
149
|
+
Then an ad is served
|
|
150
|
+
And the end-card mode is "native_endcard"
|
|
151
|
+
|
|
152
|
+
@V @validation
|
|
153
|
+
Scenario: V1 — an unknown placement key raises a typed validation error, not a silent no-fill
|
|
154
|
+
Given the app requests placement "definitely_not_a_real_placement"
|
|
155
|
+
Then the request fails
|
|
156
|
+
And the SDK surfaces a typed validation error with HTTP status 422
|
|
157
|
+
And the engine's error code is 10017
|
|
158
|
+
|
|
159
|
+
@V @validation
|
|
160
|
+
Scenario: V3 — an unknown custom-targeting key is rejected by the engine
|
|
161
|
+
Given the app requests placement "home"
|
|
162
|
+
And the custom target "not_a_configured_preset" is "x"
|
|
163
|
+
Then the request fails
|
|
164
|
+
And the SDK surfaces a typed validation error with HTTP status 422
|
|
165
|
+
And the engine's error code is 10028
|
|
166
|
+
|
|
167
|
+
@V @validation
|
|
168
|
+
Scenario: V4 — the SDK deduplicates a repeated custom key, so the engine never sees the duplicate that would fail the whole request
|
|
169
|
+
Sent raw over HTTP this same payload returns 422 code 10031, and the engine aborts
|
|
170
|
+
validation for the ENTIRE request — every placement, not just the offending target. All
|
|
171
|
+
three SDKs dedupe by key (last write wins), so a publisher cannot reach that state through
|
|
172
|
+
the public API. This scenario is the live proof of that guard: it must SERVE, and both
|
|
173
|
+
placements must come back.
|
|
174
|
+
|
|
175
|
+
Given the app requests placement "home"
|
|
176
|
+
And the app requests placement "promotions"
|
|
177
|
+
And the custom target "category" is "sports"
|
|
178
|
+
And the custom target "category" is "news"
|
|
179
|
+
Then an ad is served
|
|
180
|
+
And exactly 2 decisions are returned
|
|
181
|
+
And every requested placement comes back with its own decision
|
|
182
|
+
|
|
183
|
+
@V @validation
|
|
184
|
+
Scenario: V5 — a single valid custom target does not break serving
|
|
185
|
+
Given the app requests placement "home"
|
|
186
|
+
And the custom target "category" is "sports"
|
|
187
|
+
Then an ad is served
|
|
188
|
+
|
|
189
|
+
@V @validation
|
|
190
|
+
Scenario: V6 — a count above the documented maximum is rejected
|
|
191
|
+
Given the app requests placement "home"
|
|
192
|
+
And placement "home" asks for 99 creatives
|
|
193
|
+
Then the request fails
|
|
194
|
+
And the SDK surfaces a typed validation error with HTTP status 422
|
|
195
|
+
And the engine's error code is 10016
|
|
196
|
+
|
|
197
|
+
@V @validation
|
|
198
|
+
Scenario: V9 — a request with no placements fails locally, before any network call
|
|
199
|
+
Given the app requests no placements
|
|
200
|
+
Then the request fails
|
|
201
|
+
And the SDK rejects the request itself, before any network call
|
|
202
|
+
|
|
203
|
+
@Z @legacy-version
|
|
204
|
+
Scenario: Z1 — API version 2025-01-01 still serves a native ad and never a journey block
|
|
205
|
+
Given the app sends X-Decision-Version "2025-01-01"
|
|
206
|
+
And the app requests placement "home"
|
|
207
|
+
Then an ad is served
|
|
208
|
+
And the creative carries complete metadata: ad, creative, placement and template ids
|
|
209
|
+
And the creative is not a Journey Ad
|
|
210
|
+
|
|
211
|
+
@Z @legacy-version
|
|
212
|
+
Scenario: Z2 — omitting the version header still serves (engine falls back to the oldest active version)
|
|
213
|
+
Given the app sends no X-Decision-Version header
|
|
214
|
+
And the app requests placement "home"
|
|
215
|
+
Then an ad is served
|
|
216
|
+
And the creative is not a Journey Ad
|
|
217
|
+
|
|
218
|
+
@SP @sponsored-pin
|
|
219
|
+
Scenario: SP1 — a radius search returns the campaign's own shops, nearest first
|
|
220
|
+
Given the app sends X-Decision-Version "2025-11-01"
|
|
221
|
+
And the app requests placement "poi"
|
|
222
|
+
And the app searches for pins within 20000 m of (-33.4175, -70.6065)
|
|
223
|
+
Then an ad is served
|
|
224
|
+
And the ad carries at least 2 pins
|
|
225
|
+
And the pins are ordered nearest first
|
|
226
|
+
And every pin is one of the advertiser's locations, with a name and a distance
|
|
227
|
+
|
|
228
|
+
@SP @sponsored-pin
|
|
229
|
+
Scenario: SP2 — a bounds search returns them too — the other shape a map can ask with
|
|
230
|
+
Given the app sends X-Decision-Version "2025-11-01"
|
|
231
|
+
And the app requests placement "poi"
|
|
232
|
+
And the app searches for pins inside the map area north -33.3, south -33.55, east -70.45, west -70.65, around (-33.4175, -70.6065)
|
|
233
|
+
Then an ad is served
|
|
234
|
+
And the ad carries at least 2 pins
|
|
235
|
+
And every pin is one of the advertiser's locations, with a name and a distance
|
|
236
|
+
|
|
237
|
+
@SP @sponsored-pin
|
|
238
|
+
Scenario: SP3 — every returned shop carries its own view, tap and click beacons
|
|
239
|
+
Given the app sends X-Decision-Version "2025-11-01"
|
|
240
|
+
And the app requests placement "poi"
|
|
241
|
+
And the app searches for pins within 20000 m of (-33.4175, -70.6065)
|
|
242
|
+
Then an ad is served
|
|
243
|
+
And every pin carries its own view, tap and click beacons
|
|
244
|
+
|
|
245
|
+
@SP @sponsored-pin
|
|
246
|
+
Scenario: SP4 — every returned shop carries a resolved destination, its own or the creative's
|
|
247
|
+
Given the app sends X-Decision-Version "2025-11-01"
|
|
248
|
+
And the app requests placement "poi"
|
|
249
|
+
And the app searches for pins within 20000 m of (-33.4175, -70.6065)
|
|
250
|
+
Then an ad is served
|
|
251
|
+
And every pin carries a resolved destination URL
|
|
252
|
+
|
|
253
|
+
@SP @sponsored-pin
|
|
254
|
+
Scenario: SP5 — the request's limit narrows how many shops come back
|
|
255
|
+
Given the app sends X-Decision-Version "2025-11-01"
|
|
256
|
+
And the app requests placement "poi"
|
|
257
|
+
And the app searches for pins within 20000 m of (-33.4175, -70.6065)
|
|
258
|
+
And the search asks for at most 2 pins
|
|
259
|
+
Then an ad is served
|
|
260
|
+
And the ad carries exactly 2 pins
|
|
261
|
+
|
|
262
|
+
@SP @sponsored-pin
|
|
263
|
+
Scenario: SP6 — a tight radius excludes the shops outside it
|
|
264
|
+
Given the app sends X-Decision-Version "2025-11-01"
|
|
265
|
+
And the app requests placement "poi"
|
|
266
|
+
And the app searches for pins within 1500 m of (-33.4175, -70.6065)
|
|
267
|
+
Then an ad is served
|
|
268
|
+
And the ad carries exactly 1 pin
|
|
269
|
+
|
|
270
|
+
@SP @sponsored-pin
|
|
271
|
+
Scenario: SP7 — without a distance search a Sponsored Pin campaign does not serve at all
|
|
272
|
+
Given the app sends X-Decision-Version "2025-11-01"
|
|
273
|
+
And the app requests placement "poi"
|
|
274
|
+
Then no ad is served, and the placement answers with a clean no-fill
|
|
275
|
+
|
|
276
|
+
@SP @sponsored-pin
|
|
277
|
+
Scenario: SP8 — on the older API version the ad still serves, silently carrying no pins — the failure an integrator must recognise
|
|
278
|
+
Given the app sends no X-Decision-Version header
|
|
279
|
+
And the app requests placement "poi"
|
|
280
|
+
And the app searches for pins within 20000 m of (-33.4175, -70.6065)
|
|
281
|
+
Then an ad is served
|
|
282
|
+
And the ad carries no pins
|
|
283
|
+
|
|
284
|
+
@SP @sponsored-pin
|
|
285
|
+
Scenario: SP9 — a map's first render sends every shop's view beacon exactly once
|
|
286
|
+
Given the app sends X-Decision-Version "2025-11-01"
|
|
287
|
+
And the app requests placement "poi"
|
|
288
|
+
And the app searches for pins within 20000 m of (-33.4175, -70.6065)
|
|
289
|
+
Then an ad is served
|
|
290
|
+
And the ad carries at least 1 pin
|
|
291
|
+
And every pin carries its own view, tap and click beacons
|
|
292
|
+
When the app shows every pin
|
|
293
|
+
Then each shown pin's view beacon reaches the engine exactly once per response, and no pin the app never showed sends one
|
|
294
|
+
And the engine accepts every beacon
|
|
295
|
+
|
|
296
|
+
@SP @sponsored-pin
|
|
297
|
+
Scenario: SP10 — re-rendering the same response sends no view beacon again
|
|
298
|
+
Given the app sends X-Decision-Version "2025-11-01"
|
|
299
|
+
And the app requests placement "poi"
|
|
300
|
+
And the app searches for pins within 20000 m of (-33.4175, -70.6065)
|
|
301
|
+
Then an ad is served
|
|
302
|
+
And the ad carries at least 1 pin
|
|
303
|
+
And every pin carries its own view, tap and click beacons
|
|
304
|
+
When the app shows every pin
|
|
305
|
+
And the app shows every pin
|
|
306
|
+
And the app shows every pin
|
|
307
|
+
Then each shown pin's view beacon reaches the engine exactly once per response, and no pin the app never showed sends one
|
|
308
|
+
And the engine accepts every beacon
|
|
309
|
+
|
|
310
|
+
@SP @sponsored-pin
|
|
311
|
+
Scenario: SP11 — a render that brings new shops into view sends views only for the new ones
|
|
312
|
+
Given the app sends X-Decision-Version "2025-11-01"
|
|
313
|
+
And the app requests placement "poi"
|
|
314
|
+
And the app searches for pins within 20000 m of (-33.4175, -70.6065)
|
|
315
|
+
Then an ad is served
|
|
316
|
+
And the ad carries at least 4 pins
|
|
317
|
+
And every pin carries its own view, tap and click beacons
|
|
318
|
+
When the app shows the first 2 pins
|
|
319
|
+
And the app shows pins 2 to 4
|
|
320
|
+
And the app shows the first 4 pins
|
|
321
|
+
Then each shown pin's view beacon reaches the engine exactly once per response, and no pin the app never showed sends one
|
|
322
|
+
And the engine accepts every beacon
|
|
323
|
+
|
|
324
|
+
@SP @sponsored-pin
|
|
325
|
+
Scenario: SP12 — a new decision response reports the same shops again, once
|
|
326
|
+
Given the app sends X-Decision-Version "2025-11-01"
|
|
327
|
+
And the app requests placement "poi"
|
|
328
|
+
And the app searches for pins within 20000 m of (-33.4175, -70.6065)
|
|
329
|
+
Then an ad is served
|
|
330
|
+
And the ad carries at least 1 pin
|
|
331
|
+
And every pin carries its own view, tap and click beacons
|
|
332
|
+
When the app shows every pin
|
|
333
|
+
And the app fetches a new decision response for the same search
|
|
334
|
+
And the app shows every pin
|
|
335
|
+
And the app shows every pin
|
|
336
|
+
Then each shown pin's view beacon reaches the engine exactly once per response, and no pin the app never showed sends one
|
|
337
|
+
And a new response carries new view beacons for the same pins
|
|
338
|
+
And the engine accepts every beacon
|
|
339
|
+
|
|
340
|
+
@SP @sponsored-pin
|
|
341
|
+
Scenario: SP13 — single-pin views follow the same once-per-response rule as the bulk call
|
|
342
|
+
Given the app sends X-Decision-Version "2025-11-01"
|
|
343
|
+
And the app requests placement "poi"
|
|
344
|
+
And the app searches for pins within 20000 m of (-33.4175, -70.6065)
|
|
345
|
+
Then an ad is served
|
|
346
|
+
And the ad carries at least 1 pin
|
|
347
|
+
And every pin carries its own view, tap and click beacons
|
|
348
|
+
When the app shows pin 1 on its own
|
|
349
|
+
And the app shows pin 1 on its own
|
|
350
|
+
And the app shows every pin
|
|
351
|
+
And the app shows pin 1 on its own
|
|
352
|
+
Then each shown pin's view beacon reaches the engine exactly once per response, and no pin the app never showed sends one
|
|
353
|
+
And the engine accepts every beacon
|
|
354
|
+
|
|
355
|
+
@SP @sponsored-pin
|
|
356
|
+
Scenario: SP14 — taps and clicks are sent on every call, never de-duplicated
|
|
357
|
+
Given the app sends X-Decision-Version "2025-11-01"
|
|
358
|
+
And the app requests placement "poi"
|
|
359
|
+
And the app searches for pins within 20000 m of (-33.4175, -70.6065)
|
|
360
|
+
Then an ad is served
|
|
361
|
+
And the ad carries at least 1 pin
|
|
362
|
+
And every pin carries its own view, tap and click beacons
|
|
363
|
+
When the app shows every pin
|
|
364
|
+
And the user taps pin 1
|
|
365
|
+
And the user taps pin 1
|
|
366
|
+
And the user clicks pin 1
|
|
367
|
+
And the user clicks pin 1
|
|
368
|
+
And the app shows every pin
|
|
369
|
+
Then each shown pin's view beacon reaches the engine exactly once per response, and no pin the app never showed sends one
|
|
370
|
+
And every tap and every click sends its beacon, on every call
|
|
371
|
+
And the engine accepts every beacon
|
package/scenarios.json
ADDED
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"description": "Shared cross-SDK E2E scenario manifest. ONE definition, executed by a thin interpreter in each SDK, so the three suites cannot drift. Declarative only: given a request, expect an outcome. Sponsored Pin tracking scenarios add `pinTracking`: the point-tracking calls an app makes on the served pins, as data, and the beacons that must reach the engine. Procedural scenarios (log capture, concurrency, retry, TTL waits) stay hand-written per SDK. Mirrored byte-identically into all three repos; a checksum test fails the build if the copies diverge. Every expected value here was verified against a live local decision-engine before being written.",
|
|
4
|
+
"scenarios": [
|
|
5
|
+
{
|
|
6
|
+
"id": "L1",
|
|
7
|
+
"title": "a normal native ad serves with complete, well-formed metadata",
|
|
8
|
+
"request": {
|
|
9
|
+
"placements": [
|
|
10
|
+
{
|
|
11
|
+
"key": "home"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"expect": {
|
|
16
|
+
"outcome": "served",
|
|
17
|
+
"decisions": 1,
|
|
18
|
+
"creative": {
|
|
19
|
+
"requireMetadata": true,
|
|
20
|
+
"journey": false,
|
|
21
|
+
"formatEquals": "native",
|
|
22
|
+
"priorityIn": [
|
|
23
|
+
"standard",
|
|
24
|
+
"sponsorship",
|
|
25
|
+
"house"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "L5",
|
|
32
|
+
"title": "a placement with no eligible ad returns a clean no-ad, not an error",
|
|
33
|
+
"request": {
|
|
34
|
+
"placements": [
|
|
35
|
+
{
|
|
36
|
+
"key": "postroll"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"expect": {
|
|
41
|
+
"outcome": "noAd",
|
|
42
|
+
"decisions": 1
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "L6",
|
|
47
|
+
"title": "the advertiser block is complete (name, legalName, logoUrl)",
|
|
48
|
+
"request": {
|
|
49
|
+
"placements": [
|
|
50
|
+
{
|
|
51
|
+
"key": "home"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"expect": {
|
|
56
|
+
"outcome": "served",
|
|
57
|
+
"creative": {
|
|
58
|
+
"requireAdvertiser": true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "L7",
|
|
64
|
+
"title": "the template block carries a non-empty key",
|
|
65
|
+
"request": {
|
|
66
|
+
"placements": [
|
|
67
|
+
{
|
|
68
|
+
"key": "home"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"expect": {
|
|
73
|
+
"outcome": "served",
|
|
74
|
+
"creative": {
|
|
75
|
+
"requireTemplate": true
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "L8",
|
|
81
|
+
"title": "a served native ad exposes an impression URL, a click URL and non-empty contents",
|
|
82
|
+
"request": {
|
|
83
|
+
"placements": [
|
|
84
|
+
{
|
|
85
|
+
"key": "home"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"expect": {
|
|
90
|
+
"outcome": "served",
|
|
91
|
+
"creative": {
|
|
92
|
+
"impressions": "required",
|
|
93
|
+
"clicksAtLeast": 1,
|
|
94
|
+
"requireContents": true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "M1",
|
|
100
|
+
"title": "count=2 returns at most two creatives, and they are distinct",
|
|
101
|
+
"request": {
|
|
102
|
+
"placements": [
|
|
103
|
+
{
|
|
104
|
+
"key": "promotions",
|
|
105
|
+
"count": 2
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
"expect": {
|
|
110
|
+
"outcome": "served",
|
|
111
|
+
"decisions": 1,
|
|
112
|
+
"creativesAtMost": 2,
|
|
113
|
+
"creativesDistinct": true
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"id": "M2",
|
|
118
|
+
"title": "three placements in one request each get their own correctly-keyed decision",
|
|
119
|
+
"request": {
|
|
120
|
+
"placements": [
|
|
121
|
+
{
|
|
122
|
+
"key": "home"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"key": "promotions"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"key": "search"
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
"expect": {
|
|
133
|
+
"outcome": "served",
|
|
134
|
+
"decisions": 3,
|
|
135
|
+
"decisionsMatchRequest": true
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"id": "M3",
|
|
140
|
+
"title": "an advertiserId filter still serves that advertiser's ad",
|
|
141
|
+
"request": {
|
|
142
|
+
"placements": [
|
|
143
|
+
{
|
|
144
|
+
"key": "promotions",
|
|
145
|
+
"advertiserId": "advertiser_01JEPGMJXARVYSJA0CAM9FQ79Y"
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
"expect": {
|
|
150
|
+
"outcome": "served",
|
|
151
|
+
"creative": {
|
|
152
|
+
"requireAdvertiser": true
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"id": "M5",
|
|
158
|
+
"title": "format=native on a mixed placement returns a native ad",
|
|
159
|
+
"request": {
|
|
160
|
+
"placements": [
|
|
161
|
+
{
|
|
162
|
+
"key": "promotions",
|
|
163
|
+
"format": "native"
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
},
|
|
167
|
+
"expect": {
|
|
168
|
+
"outcome": "served",
|
|
169
|
+
"creative": {
|
|
170
|
+
"formatEquals": "native"
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"id": "M6",
|
|
176
|
+
"title": "format=video on the same mixed placement returns a video ad instead",
|
|
177
|
+
"request": {
|
|
178
|
+
"placements": [
|
|
179
|
+
{
|
|
180
|
+
"key": "promotions",
|
|
181
|
+
"format": "video"
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
"expect": {
|
|
186
|
+
"outcome": "served",
|
|
187
|
+
"creative": {
|
|
188
|
+
"formatEquals": "video"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"id": "M7",
|
|
194
|
+
"title": "omitting format still serves \u2014 the engine chooses",
|
|
195
|
+
"request": {
|
|
196
|
+
"placements": [
|
|
197
|
+
{
|
|
198
|
+
"key": "promotions"
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
},
|
|
202
|
+
"expect": {
|
|
203
|
+
"outcome": "served"
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"id": "M8",
|
|
208
|
+
"title": "a journey-owned placement and a normal placement resolve independently in ONE request",
|
|
209
|
+
"request": {
|
|
210
|
+
"placements": [
|
|
211
|
+
{
|
|
212
|
+
"key": "promotions"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"key": "home"
|
|
216
|
+
}
|
|
217
|
+
],
|
|
218
|
+
"session": "auto",
|
|
219
|
+
"journeyOpt": "in"
|
|
220
|
+
},
|
|
221
|
+
"expect": {
|
|
222
|
+
"outcome": "served",
|
|
223
|
+
"decisions": 2
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"id": "P1",
|
|
228
|
+
"title": "a non-skippable JSON video exposes json delivery and five video events (no skip)",
|
|
229
|
+
"request": {
|
|
230
|
+
"placements": [
|
|
231
|
+
{
|
|
232
|
+
"key": "json_none"
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
},
|
|
236
|
+
"expect": {
|
|
237
|
+
"outcome": "served",
|
|
238
|
+
"creative": {
|
|
239
|
+
"deliveryEquals": "json",
|
|
240
|
+
"formatEquals": "video",
|
|
241
|
+
"videoEventCount": 5,
|
|
242
|
+
"videoEventKeys": [
|
|
243
|
+
"start",
|
|
244
|
+
"first_quartile",
|
|
245
|
+
"midpoint",
|
|
246
|
+
"third_quartile",
|
|
247
|
+
"complete"
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"id": "P2",
|
|
254
|
+
"title": "a vast_tag ad exposes its tag URL and surfaces NO engine-side beacons (no double count)",
|
|
255
|
+
"request": {
|
|
256
|
+
"placements": [
|
|
257
|
+
{
|
|
258
|
+
"key": "vasttag_none"
|
|
259
|
+
}
|
|
260
|
+
]
|
|
261
|
+
},
|
|
262
|
+
"expect": {
|
|
263
|
+
"outcome": "served",
|
|
264
|
+
"creative": {
|
|
265
|
+
"deliveryEquals": "vast_tag",
|
|
266
|
+
"requireVastTagUrl": true,
|
|
267
|
+
"videoEventCount": 0,
|
|
268
|
+
"impressions": "forbidden"
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"id": "P3",
|
|
274
|
+
"title": "a vast_xml ad exposes base64 that decodes to a VAST document, and no beacons",
|
|
275
|
+
"request": {
|
|
276
|
+
"placements": [
|
|
277
|
+
{
|
|
278
|
+
"key": "vastxml_none"
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
},
|
|
282
|
+
"expect": {
|
|
283
|
+
"outcome": "served",
|
|
284
|
+
"creative": {
|
|
285
|
+
"deliveryEquals": "vast_xml",
|
|
286
|
+
"requireVastXmlDecodes": true,
|
|
287
|
+
"videoEventCount": 0,
|
|
288
|
+
"impressions": "forbidden"
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"id": "P6",
|
|
294
|
+
"title": "a skippable video resolves skippability from ENGINE METADATA, plus a sixth skip event",
|
|
295
|
+
"request": {
|
|
296
|
+
"placements": [
|
|
297
|
+
{
|
|
298
|
+
"key": "json_none_skippable"
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
},
|
|
302
|
+
"expect": {
|
|
303
|
+
"outcome": "served",
|
|
304
|
+
"creative": {
|
|
305
|
+
"metadataIsSkippable": true,
|
|
306
|
+
"skipOffsetSecondsEquals": 5,
|
|
307
|
+
"helperIsSkippable": true,
|
|
308
|
+
"helperSkipOffsetEquals": "5",
|
|
309
|
+
"videoEventCount": 6
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"id": "P8",
|
|
315
|
+
"title": "an end-card video reports its configured endCardMode",
|
|
316
|
+
"request": {
|
|
317
|
+
"placements": [
|
|
318
|
+
{
|
|
319
|
+
"key": "json_native_endcard"
|
|
320
|
+
}
|
|
321
|
+
]
|
|
322
|
+
},
|
|
323
|
+
"expect": {
|
|
324
|
+
"outcome": "served",
|
|
325
|
+
"creative": {
|
|
326
|
+
"endCardModeEquals": "native_endcard"
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
"id": "V1",
|
|
332
|
+
"title": "an unknown placement key raises a typed validation error, not a silent no-fill",
|
|
333
|
+
"request": {
|
|
334
|
+
"placements": [
|
|
335
|
+
{
|
|
336
|
+
"key": "definitely_not_a_real_placement"
|
|
337
|
+
}
|
|
338
|
+
]
|
|
339
|
+
},
|
|
340
|
+
"expect": {
|
|
341
|
+
"outcome": "error",
|
|
342
|
+
"error": {
|
|
343
|
+
"kind": "validation",
|
|
344
|
+
"status": 422,
|
|
345
|
+
"code": 10017
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"id": "V3",
|
|
351
|
+
"title": "an unknown custom-targeting key is rejected by the engine",
|
|
352
|
+
"request": {
|
|
353
|
+
"placements": [
|
|
354
|
+
{
|
|
355
|
+
"key": "home"
|
|
356
|
+
}
|
|
357
|
+
],
|
|
358
|
+
"custom": [
|
|
359
|
+
{
|
|
360
|
+
"key": "not_a_configured_preset",
|
|
361
|
+
"value": "x"
|
|
362
|
+
}
|
|
363
|
+
]
|
|
364
|
+
},
|
|
365
|
+
"expect": {
|
|
366
|
+
"outcome": "error",
|
|
367
|
+
"error": {
|
|
368
|
+
"kind": "validation",
|
|
369
|
+
"status": 422,
|
|
370
|
+
"code": 10028
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"id": "V4",
|
|
376
|
+
"title": "the SDK deduplicates a repeated custom key, so the engine never sees the duplicate that would fail the whole request",
|
|
377
|
+
"comment": "Sent raw over HTTP this same payload returns 422 code 10031, and the engine aborts validation for the ENTIRE request \u2014 every placement, not just the offending target. All three SDKs dedupe by key (last write wins), so a publisher cannot reach that state through the public API. This scenario is the live proof of that guard: it must SERVE, and both placements must come back.",
|
|
378
|
+
"request": {
|
|
379
|
+
"placements": [
|
|
380
|
+
{
|
|
381
|
+
"key": "home"
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"key": "promotions"
|
|
385
|
+
}
|
|
386
|
+
],
|
|
387
|
+
"custom": [
|
|
388
|
+
{
|
|
389
|
+
"key": "category",
|
|
390
|
+
"value": "sports"
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"key": "category",
|
|
394
|
+
"value": "news"
|
|
395
|
+
}
|
|
396
|
+
]
|
|
397
|
+
},
|
|
398
|
+
"expect": {
|
|
399
|
+
"outcome": "served",
|
|
400
|
+
"decisions": 2,
|
|
401
|
+
"decisionsMatchRequest": true
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
"id": "V5",
|
|
406
|
+
"title": "a single valid custom target does not break serving",
|
|
407
|
+
"request": {
|
|
408
|
+
"placements": [
|
|
409
|
+
{
|
|
410
|
+
"key": "home"
|
|
411
|
+
}
|
|
412
|
+
],
|
|
413
|
+
"custom": [
|
|
414
|
+
{
|
|
415
|
+
"key": "category",
|
|
416
|
+
"value": "sports"
|
|
417
|
+
}
|
|
418
|
+
]
|
|
419
|
+
},
|
|
420
|
+
"expect": {
|
|
421
|
+
"outcome": "served"
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
"id": "V6",
|
|
426
|
+
"title": "a count above the documented maximum is rejected",
|
|
427
|
+
"request": {
|
|
428
|
+
"placements": [
|
|
429
|
+
{
|
|
430
|
+
"key": "home",
|
|
431
|
+
"count": 99
|
|
432
|
+
}
|
|
433
|
+
]
|
|
434
|
+
},
|
|
435
|
+
"expect": {
|
|
436
|
+
"outcome": "error",
|
|
437
|
+
"error": {
|
|
438
|
+
"kind": "validation",
|
|
439
|
+
"status": 422,
|
|
440
|
+
"code": 10016
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
"id": "V9",
|
|
446
|
+
"title": "a request with no placements fails locally, before any network call",
|
|
447
|
+
"request": {
|
|
448
|
+
"placements": []
|
|
449
|
+
},
|
|
450
|
+
"expect": {
|
|
451
|
+
"outcome": "error",
|
|
452
|
+
"error": {
|
|
453
|
+
"kind": "local"
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
"id": "Z1",
|
|
459
|
+
"title": "API version 2025-01-01 still serves a native ad and never a journey block",
|
|
460
|
+
"request": {
|
|
461
|
+
"placements": [
|
|
462
|
+
{
|
|
463
|
+
"key": "home"
|
|
464
|
+
}
|
|
465
|
+
],
|
|
466
|
+
"apiVersion": "2025-01-01"
|
|
467
|
+
},
|
|
468
|
+
"expect": {
|
|
469
|
+
"outcome": "served",
|
|
470
|
+
"creative": {
|
|
471
|
+
"journey": false,
|
|
472
|
+
"requireMetadata": true
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
"id": "Z2",
|
|
478
|
+
"title": "omitting the version header still serves (engine falls back to the oldest active version)",
|
|
479
|
+
"request": {
|
|
480
|
+
"placements": [
|
|
481
|
+
{
|
|
482
|
+
"key": "home"
|
|
483
|
+
}
|
|
484
|
+
],
|
|
485
|
+
"apiVersion": "none"
|
|
486
|
+
},
|
|
487
|
+
"expect": {
|
|
488
|
+
"outcome": "served",
|
|
489
|
+
"creative": {
|
|
490
|
+
"journey": false
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
"id": "SP1",
|
|
496
|
+
"title": "a radius search returns the campaign's own shops, nearest first",
|
|
497
|
+
"request": {
|
|
498
|
+
"apiVersion": "2025-11-01",
|
|
499
|
+
"placements": [
|
|
500
|
+
{
|
|
501
|
+
"key": "poi"
|
|
502
|
+
}
|
|
503
|
+
],
|
|
504
|
+
"distance": {
|
|
505
|
+
"latitude": -33.4175,
|
|
506
|
+
"longitude": -70.6065,
|
|
507
|
+
"radius": 20000
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
"expect": {
|
|
511
|
+
"outcome": "served",
|
|
512
|
+
"creative": {
|
|
513
|
+
"matchedPointsAtLeast": 2,
|
|
514
|
+
"matchedPointsNearestFirst": true,
|
|
515
|
+
"matchedPointsWellFormed": true
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
"id": "SP2",
|
|
521
|
+
"title": "a bounds search returns them too \u2014 the other shape a map can ask with",
|
|
522
|
+
"request": {
|
|
523
|
+
"apiVersion": "2025-11-01",
|
|
524
|
+
"placements": [
|
|
525
|
+
{
|
|
526
|
+
"key": "poi"
|
|
527
|
+
}
|
|
528
|
+
],
|
|
529
|
+
"distance": {
|
|
530
|
+
"latitude": -33.4175,
|
|
531
|
+
"longitude": -70.6065,
|
|
532
|
+
"bounds": {
|
|
533
|
+
"north": -33.3,
|
|
534
|
+
"south": -33.55,
|
|
535
|
+
"east": -70.45,
|
|
536
|
+
"west": -70.65
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
"expect": {
|
|
541
|
+
"outcome": "served",
|
|
542
|
+
"creative": {
|
|
543
|
+
"matchedPointsAtLeast": 2,
|
|
544
|
+
"matchedPointsWellFormed": true
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
"id": "SP3",
|
|
550
|
+
"title": "every returned shop carries its own view, tap and click beacons",
|
|
551
|
+
"request": {
|
|
552
|
+
"apiVersion": "2025-11-01",
|
|
553
|
+
"placements": [
|
|
554
|
+
{
|
|
555
|
+
"key": "poi"
|
|
556
|
+
}
|
|
557
|
+
],
|
|
558
|
+
"distance": {
|
|
559
|
+
"latitude": -33.4175,
|
|
560
|
+
"longitude": -70.6065,
|
|
561
|
+
"radius": 20000
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
"expect": {
|
|
565
|
+
"outcome": "served",
|
|
566
|
+
"creative": {
|
|
567
|
+
"matchedPointsHaveBeacons": true
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
"id": "SP4",
|
|
573
|
+
"title": "every returned shop carries a resolved destination, its own or the creative's",
|
|
574
|
+
"request": {
|
|
575
|
+
"apiVersion": "2025-11-01",
|
|
576
|
+
"placements": [
|
|
577
|
+
{
|
|
578
|
+
"key": "poi"
|
|
579
|
+
}
|
|
580
|
+
],
|
|
581
|
+
"distance": {
|
|
582
|
+
"latitude": -33.4175,
|
|
583
|
+
"longitude": -70.6065,
|
|
584
|
+
"radius": 20000
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
"expect": {
|
|
588
|
+
"outcome": "served",
|
|
589
|
+
"creative": {
|
|
590
|
+
"matchedPointsHaveClickUrl": true
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
"id": "SP5",
|
|
596
|
+
"title": "the request's limit narrows how many shops come back",
|
|
597
|
+
"request": {
|
|
598
|
+
"apiVersion": "2025-11-01",
|
|
599
|
+
"placements": [
|
|
600
|
+
{
|
|
601
|
+
"key": "poi"
|
|
602
|
+
}
|
|
603
|
+
],
|
|
604
|
+
"distance": {
|
|
605
|
+
"latitude": -33.4175,
|
|
606
|
+
"longitude": -70.6065,
|
|
607
|
+
"radius": 20000,
|
|
608
|
+
"limit": 2
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
"expect": {
|
|
612
|
+
"outcome": "served",
|
|
613
|
+
"creative": {
|
|
614
|
+
"matchedPointsEquals": 2
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
"id": "SP6",
|
|
620
|
+
"title": "a tight radius excludes the shops outside it",
|
|
621
|
+
"request": {
|
|
622
|
+
"apiVersion": "2025-11-01",
|
|
623
|
+
"placements": [
|
|
624
|
+
{
|
|
625
|
+
"key": "poi"
|
|
626
|
+
}
|
|
627
|
+
],
|
|
628
|
+
"distance": {
|
|
629
|
+
"latitude": -33.4175,
|
|
630
|
+
"longitude": -70.6065,
|
|
631
|
+
"radius": 1500
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
"expect": {
|
|
635
|
+
"outcome": "served",
|
|
636
|
+
"creative": {
|
|
637
|
+
"matchedPointsEquals": 1
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
"id": "SP7",
|
|
643
|
+
"title": "without a distance search a Sponsored Pin campaign does not serve at all",
|
|
644
|
+
"request": {
|
|
645
|
+
"apiVersion": "2025-11-01",
|
|
646
|
+
"placements": [
|
|
647
|
+
{
|
|
648
|
+
"key": "poi"
|
|
649
|
+
}
|
|
650
|
+
]
|
|
651
|
+
},
|
|
652
|
+
"expect": {
|
|
653
|
+
"outcome": "noAd"
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
"id": "SP8",
|
|
658
|
+
"title": "on the older API version the ad still serves, silently carrying no pins \u2014 the failure an integrator must recognise",
|
|
659
|
+
"request": {
|
|
660
|
+
"apiVersion": "none",
|
|
661
|
+
"placements": [
|
|
662
|
+
{
|
|
663
|
+
"key": "poi"
|
|
664
|
+
}
|
|
665
|
+
],
|
|
666
|
+
"distance": {
|
|
667
|
+
"latitude": -33.4175,
|
|
668
|
+
"longitude": -70.6065,
|
|
669
|
+
"radius": 20000
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
"expect": {
|
|
673
|
+
"outcome": "served",
|
|
674
|
+
"creative": {
|
|
675
|
+
"matchedPointsNone": true
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
"id": "SP9",
|
|
681
|
+
"title": "a map's first render sends every shop's view beacon exactly once",
|
|
682
|
+
"request": {
|
|
683
|
+
"apiVersion": "2025-11-01",
|
|
684
|
+
"placements": [
|
|
685
|
+
{
|
|
686
|
+
"key": "poi"
|
|
687
|
+
}
|
|
688
|
+
],
|
|
689
|
+
"distance": {
|
|
690
|
+
"latitude": -33.4175,
|
|
691
|
+
"longitude": -70.6065,
|
|
692
|
+
"radius": 20000
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
"expect": {
|
|
696
|
+
"outcome": "served",
|
|
697
|
+
"creative": {
|
|
698
|
+
"matchedPointsAtLeast": 1,
|
|
699
|
+
"matchedPointsHaveBeacons": true
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
"pinTracking": {
|
|
703
|
+
"steps": [
|
|
704
|
+
{
|
|
705
|
+
"views": "all"
|
|
706
|
+
}
|
|
707
|
+
],
|
|
708
|
+
"expect": {
|
|
709
|
+
"viewsOncePerResponse": true,
|
|
710
|
+
"engineAcceptsEveryBeacon": true
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
"id": "SP10",
|
|
716
|
+
"title": "re-rendering the same response sends no view beacon again",
|
|
717
|
+
"request": {
|
|
718
|
+
"apiVersion": "2025-11-01",
|
|
719
|
+
"placements": [
|
|
720
|
+
{
|
|
721
|
+
"key": "poi"
|
|
722
|
+
}
|
|
723
|
+
],
|
|
724
|
+
"distance": {
|
|
725
|
+
"latitude": -33.4175,
|
|
726
|
+
"longitude": -70.6065,
|
|
727
|
+
"radius": 20000
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
"expect": {
|
|
731
|
+
"outcome": "served",
|
|
732
|
+
"creative": {
|
|
733
|
+
"matchedPointsAtLeast": 1,
|
|
734
|
+
"matchedPointsHaveBeacons": true
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
"pinTracking": {
|
|
738
|
+
"steps": [
|
|
739
|
+
{
|
|
740
|
+
"views": "all"
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
"views": "all"
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
"views": "all"
|
|
747
|
+
}
|
|
748
|
+
],
|
|
749
|
+
"expect": {
|
|
750
|
+
"viewsOncePerResponse": true,
|
|
751
|
+
"engineAcceptsEveryBeacon": true
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
"id": "SP11",
|
|
757
|
+
"title": "a render that brings new shops into view sends views only for the new ones",
|
|
758
|
+
"request": {
|
|
759
|
+
"apiVersion": "2025-11-01",
|
|
760
|
+
"placements": [
|
|
761
|
+
{
|
|
762
|
+
"key": "poi"
|
|
763
|
+
}
|
|
764
|
+
],
|
|
765
|
+
"distance": {
|
|
766
|
+
"latitude": -33.4175,
|
|
767
|
+
"longitude": -70.6065,
|
|
768
|
+
"radius": 20000
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
"expect": {
|
|
772
|
+
"outcome": "served",
|
|
773
|
+
"creative": {
|
|
774
|
+
"matchedPointsAtLeast": 4,
|
|
775
|
+
"matchedPointsHaveBeacons": true
|
|
776
|
+
}
|
|
777
|
+
},
|
|
778
|
+
"pinTracking": {
|
|
779
|
+
"steps": [
|
|
780
|
+
{
|
|
781
|
+
"views": {
|
|
782
|
+
"from": 0,
|
|
783
|
+
"to": 2
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
"views": {
|
|
788
|
+
"from": 1,
|
|
789
|
+
"to": 4
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
"views": {
|
|
794
|
+
"from": 0,
|
|
795
|
+
"to": 4
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
],
|
|
799
|
+
"expect": {
|
|
800
|
+
"viewsOncePerResponse": true,
|
|
801
|
+
"engineAcceptsEveryBeacon": true
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
"id": "SP12",
|
|
807
|
+
"title": "a new decision response reports the same shops again, once",
|
|
808
|
+
"request": {
|
|
809
|
+
"apiVersion": "2025-11-01",
|
|
810
|
+
"placements": [
|
|
811
|
+
{
|
|
812
|
+
"key": "poi"
|
|
813
|
+
}
|
|
814
|
+
],
|
|
815
|
+
"distance": {
|
|
816
|
+
"latitude": -33.4175,
|
|
817
|
+
"longitude": -70.6065,
|
|
818
|
+
"radius": 20000
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
"expect": {
|
|
822
|
+
"outcome": "served",
|
|
823
|
+
"creative": {
|
|
824
|
+
"matchedPointsAtLeast": 1,
|
|
825
|
+
"matchedPointsHaveBeacons": true
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
"pinTracking": {
|
|
829
|
+
"steps": [
|
|
830
|
+
{
|
|
831
|
+
"views": "all"
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
"newResponse": true
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
"views": "all"
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
"views": "all"
|
|
841
|
+
}
|
|
842
|
+
],
|
|
843
|
+
"expect": {
|
|
844
|
+
"viewsOncePerResponse": true,
|
|
845
|
+
"newResponseHasNewViewUrls": true,
|
|
846
|
+
"engineAcceptsEveryBeacon": true
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
"id": "SP13",
|
|
852
|
+
"title": "single-pin views follow the same once-per-response rule as the bulk call",
|
|
853
|
+
"request": {
|
|
854
|
+
"apiVersion": "2025-11-01",
|
|
855
|
+
"placements": [
|
|
856
|
+
{
|
|
857
|
+
"key": "poi"
|
|
858
|
+
}
|
|
859
|
+
],
|
|
860
|
+
"distance": {
|
|
861
|
+
"latitude": -33.4175,
|
|
862
|
+
"longitude": -70.6065,
|
|
863
|
+
"radius": 20000
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
"expect": {
|
|
867
|
+
"outcome": "served",
|
|
868
|
+
"creative": {
|
|
869
|
+
"matchedPointsAtLeast": 1,
|
|
870
|
+
"matchedPointsHaveBeacons": true
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
"pinTracking": {
|
|
874
|
+
"steps": [
|
|
875
|
+
{
|
|
876
|
+
"view": 0
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
"view": 0
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
"views": "all"
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
"view": 0
|
|
886
|
+
}
|
|
887
|
+
],
|
|
888
|
+
"expect": {
|
|
889
|
+
"viewsOncePerResponse": true,
|
|
890
|
+
"engineAcceptsEveryBeacon": true
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
"id": "SP14",
|
|
896
|
+
"title": "taps and clicks are sent on every call, never de-duplicated",
|
|
897
|
+
"request": {
|
|
898
|
+
"apiVersion": "2025-11-01",
|
|
899
|
+
"placements": [
|
|
900
|
+
{
|
|
901
|
+
"key": "poi"
|
|
902
|
+
}
|
|
903
|
+
],
|
|
904
|
+
"distance": {
|
|
905
|
+
"latitude": -33.4175,
|
|
906
|
+
"longitude": -70.6065,
|
|
907
|
+
"radius": 20000
|
|
908
|
+
}
|
|
909
|
+
},
|
|
910
|
+
"expect": {
|
|
911
|
+
"outcome": "served",
|
|
912
|
+
"creative": {
|
|
913
|
+
"matchedPointsAtLeast": 1,
|
|
914
|
+
"matchedPointsHaveBeacons": true
|
|
915
|
+
}
|
|
916
|
+
},
|
|
917
|
+
"pinTracking": {
|
|
918
|
+
"steps": [
|
|
919
|
+
{
|
|
920
|
+
"views": "all"
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
"tap": 0
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
"tap": 0
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
"click": 0
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
"click": 0
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
"views": "all"
|
|
936
|
+
}
|
|
937
|
+
],
|
|
938
|
+
"expect": {
|
|
939
|
+
"viewsOncePerResponse": true,
|
|
940
|
+
"interactionsEveryCall": true,
|
|
941
|
+
"engineAcceptsEveryBeacon": true
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
]
|
|
946
|
+
}
|