@revclamp/google-ads-transparency-mcp-actor 1.0.1 → 1.0.3
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/.actor/actor.json +63 -2
- package/.actor/input_schema.json +4 -1
- package/actor_config.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/apify_actor.mjs +347 -79
- package/src/mcp_server.mjs +1 -1
- package/src/rpc_client.mjs +36 -7
package/.actor/actor.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"actorSpecification": 1,
|
|
3
3
|
"name": "google-ads-transparency-mcp-actor",
|
|
4
|
-
"title": "RevClamp AdPulse -
|
|
5
|
-
"description": "
|
|
4
|
+
"title": "RevClamp AdPulse - Google Ads Transparency MCP & Actor",
|
|
5
|
+
"description": "Resolve verified Google Ads advertiser entities from any brand or domain and extract active or historical ad creatives (Text, Image, Video), ad copy, and run dates with zero browser overhead. Independent intelligence tool by RevClamp; not affiliated with or endorsed by Google LLC.",
|
|
6
6
|
"version": "1.0",
|
|
7
7
|
"buildTag": "latest",
|
|
8
8
|
"minMemoryMbytes": 256,
|
|
@@ -13,5 +13,66 @@
|
|
|
13
13
|
"memoryMbytes": 256
|
|
14
14
|
},
|
|
15
15
|
"input": "./input_schema.json",
|
|
16
|
+
"output": {
|
|
17
|
+
"actorOutputSchemaVersion": 1,
|
|
18
|
+
"title": "Google Ads Transparency Creatives & Run Summary",
|
|
19
|
+
"description": "Extracted Google Ads Transparency creative records (Default Dataset) and run billing/telemetry summary (Key-Value Store OUTPUT).",
|
|
20
|
+
"properties": {
|
|
21
|
+
"creatives": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"title": "Extracted Ad Creatives",
|
|
24
|
+
"description": "Verified advertiser ad creatives (Text, Image, Video), ad copy, destination domains, and active run dates.",
|
|
25
|
+
"template": "{{links.apiDefaultDatasetUrl}}/items"
|
|
26
|
+
},
|
|
27
|
+
"runSummary": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"title": "Run Summary & Telemetry (OUTPUT)",
|
|
30
|
+
"description": "Structured execution summary, resolved advertiser entities, pagination cursor, and billing receipt summary.",
|
|
31
|
+
"template": "{{links.apiDefaultKeyValueStoreUrl}}/records/OUTPUT"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"storages": {
|
|
36
|
+
"dataset": {
|
|
37
|
+
"actorSpecification": 1,
|
|
38
|
+
"fields": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"advertiserId": { "type": "string", "title": "Advertiser ID" },
|
|
42
|
+
"advertiserName": { "type": "string", "title": "Advertiser Name" },
|
|
43
|
+
"creativeId": { "type": "string", "title": "Creative ID" },
|
|
44
|
+
"format": { "type": "string", "title": "Ad Format" },
|
|
45
|
+
"regionCode": { "type": "string", "title": "Region Code" },
|
|
46
|
+
"firstSeenAtIso": { "type": "string", "title": "First Seen (ISO)" },
|
|
47
|
+
"lastSeenAtIso": { "type": "string", "title": "Last Seen (ISO)" },
|
|
48
|
+
"daysActive": { "type": "integer", "title": "Days Active" },
|
|
49
|
+
"previewUrl": { "type": "string", "title": "Preview Asset URL" },
|
|
50
|
+
"transparencyUrl": { "type": "string", "title": "Transparency Center URL" }
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"views": {
|
|
54
|
+
"overview": {
|
|
55
|
+
"title": "Ad Creatives Overview",
|
|
56
|
+
"transformation": {
|
|
57
|
+
"fields": [
|
|
58
|
+
"advertiserId",
|
|
59
|
+
"advertiserName",
|
|
60
|
+
"creativeId",
|
|
61
|
+
"format",
|
|
62
|
+
"regionCode",
|
|
63
|
+
"firstSeenAtIso",
|
|
64
|
+
"lastSeenAtIso",
|
|
65
|
+
"daysActive",
|
|
66
|
+
"previewUrl",
|
|
67
|
+
"transparencyUrl"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"display": {
|
|
71
|
+
"component": "table"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
16
77
|
"dockerfile": "../Dockerfile"
|
|
17
78
|
}
|
package/.actor/input_schema.json
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"description": "Brand name or domain to match against verified Google Ads Transparency advertiser entities (e.g. 'nike.com', 'Stripe', 'shopify.com').",
|
|
10
10
|
"maxLength": 512,
|
|
11
11
|
"editor": "textfield",
|
|
12
|
+
"default": "Stripe",
|
|
13
|
+
"prefill": "Stripe",
|
|
12
14
|
"example": "nike.com"
|
|
13
15
|
},
|
|
14
16
|
"advertiserId": {
|
|
@@ -62,7 +64,8 @@
|
|
|
62
64
|
"description": "Maximum number of deduplicated ad creatives to extract in this run (1 to 200).",
|
|
63
65
|
"minimum": 1,
|
|
64
66
|
"maximum": 200,
|
|
65
|
-
"default": 40
|
|
67
|
+
"default": 40,
|
|
68
|
+
"prefill": 10
|
|
66
69
|
},
|
|
67
70
|
"pageToken": {
|
|
68
71
|
"title": "Next-Page Cursor Token (Optional)",
|
package/actor_config.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"manifest": {
|
|
23
23
|
"name": "google-ads-transparency-mcp-actor",
|
|
24
|
-
"version": "1.0.
|
|
24
|
+
"version": "1.0.3",
|
|
25
25
|
"title": "RevClamp AdPulse - For Google Ads Transparency Center (MCP & Apify Actor)",
|
|
26
26
|
"websiteUrl": "https://revclamp.com",
|
|
27
27
|
"description": "Find verified Google Ads advertiser entities and extract active or historical ad creatives (Text, Image, Video), preview links, and run dates without browser or proxy overhead. Independent intelligence tool by RevClamp; not affiliated with or endorsed by Google LLC."
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revclamp/google-ads-transparency-mcp-actor",
|
|
3
3
|
"mcpName": "com.revclamp/google-ads-transparency-mcp-actor",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "RevClamp AdPulse - For Google Ads Transparency Center (MCP & Apify Actor). Independent intelligence tool by RevClamp; not affiliated with or endorsed by Google LLC.",
|
|
7
7
|
"homepage": "https://revclamp.com",
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "com.revclamp/google-ads-transparency-mcp-actor",
|
|
4
4
|
"title": "RevClamp AdPulse (Google Ads Transparency MCP)",
|
|
5
5
|
"description": "Google Ads Transparency Center advertiser & creative intelligence MCP server by RevClamp.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.3",
|
|
7
7
|
"websiteUrl": "https://revclamp.com",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/lhphong09t2/revclamp-mcp",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"registryType": "npm",
|
|
28
28
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
29
29
|
"identifier": "@revclamp/google-ads-transparency-mcp-actor",
|
|
30
|
-
"version": "1.0.
|
|
30
|
+
"version": "1.0.3",
|
|
31
31
|
"transport": {
|
|
32
32
|
"type": "stdio"
|
|
33
33
|
}
|
package/src/apify_actor.mjs
CHANGED
|
@@ -82,12 +82,25 @@ export function computeRunEconomicsTelemetry({
|
|
|
82
82
|
Number(process.env.ACTOR_MEMORY_MBYTES || process.env.APIFY_MEMORY_MBYTES) ||
|
|
83
83
|
ACTOR_PRICING_CONTRACT.maxMemoryMb,
|
|
84
84
|
requestCount = 1,
|
|
85
|
+
unitPriceUsdc = ACTOR_PRICING_CONTRACT.priceUsdc,
|
|
86
|
+
actorStartFeeUsdc = ACTOR_PRICING_CONTRACT.actorStartFeeUsdc,
|
|
85
87
|
} = {}) {
|
|
86
88
|
const safeCount = Math.max(0, Math.floor(Number(billableCount) || 0));
|
|
87
89
|
const safeChargedCount = Math.max(0, Math.floor(Number(chargedEventCount) || 0));
|
|
88
90
|
const safeDurationMs = Math.max(1, Number(durationMs) || 1);
|
|
89
91
|
const safeMemoryMb = Math.min(1024, Math.max(64, Number(memoryMb) || 256));
|
|
90
92
|
const safeRequests = Math.max(1, Math.floor(Number(requestCount) || 1));
|
|
93
|
+
const resolvedUnitPriceUsdc =
|
|
94
|
+
Number.isFinite(Number(unitPriceUsdc)) && Number(unitPriceUsdc) > 0
|
|
95
|
+
? Number(unitPriceUsdc)
|
|
96
|
+
: ACTOR_PRICING_CONTRACT.priceUsdc;
|
|
97
|
+
const resolvedNetPriceUsdc = Number(
|
|
98
|
+
(resolvedUnitPriceUsdc * (1 - ACTOR_PRICING_CONTRACT.platformFeeRate)).toFixed(6)
|
|
99
|
+
);
|
|
100
|
+
const resolvedStartFeeUsdc =
|
|
101
|
+
Number.isFinite(Number(actorStartFeeUsdc)) && Number(actorStartFeeUsdc) >= 0
|
|
102
|
+
? Number(actorStartFeeUsdc)
|
|
103
|
+
: ACTOR_PRICING_CONTRACT.actorStartFeeUsdc;
|
|
91
104
|
|
|
92
105
|
// Apify compute rate: $0.30 per GB-hour; 256MB = 0.25 GB -> $0.075/hour -> $0.00002083/sec
|
|
93
106
|
const computeHours = safeDurationMs / 3600000;
|
|
@@ -95,10 +108,9 @@ export function computeRunEconomicsTelemetry({
|
|
|
95
108
|
(safeMemoryMb / 1024) * computeHours * ACTOR_PRICING_CONTRACT.computeRatePerGbHourUsdc;
|
|
96
109
|
const networkOverheadUsdc = safeRequests * 0.0000002;
|
|
97
110
|
const totalRunCostUsdc = Number((computeCostUsdc + networkOverheadUsdc).toFixed(8));
|
|
98
|
-
const actorStartFeeUsdc = ACTOR_PRICING_CONTRACT.actorStartFeeUsdc;
|
|
99
111
|
|
|
100
112
|
const configuredFloorMultiplier = Number(
|
|
101
|
-
(
|
|
113
|
+
(resolvedNetPriceUsdc / ACTOR_PRICING_CONTRACT.costCeilingUsdc).toFixed(2)
|
|
102
114
|
);
|
|
103
115
|
|
|
104
116
|
if (safeCount === 0) {
|
|
@@ -109,7 +121,7 @@ export function computeRunEconomicsTelemetry({
|
|
|
109
121
|
durationMs: safeDurationMs,
|
|
110
122
|
memoryMb: safeMemoryMb,
|
|
111
123
|
requestCount: safeRequests,
|
|
112
|
-
actorStartFeeUsdc,
|
|
124
|
+
actorStartFeeUsdc: resolvedStartFeeUsdc,
|
|
113
125
|
totalRunCostUsdc,
|
|
114
126
|
actualCostPerEventUsdc: 0,
|
|
115
127
|
costCeilingUsdc: ACTOR_PRICING_CONTRACT.costCeilingUsdc,
|
|
@@ -123,11 +135,11 @@ export function computeRunEconomicsTelemetry({
|
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
const actualCostPerEventUsdc = Number((totalRunCostUsdc / safeCount).toFixed(8));
|
|
126
|
-
const grossRevenueUsdc = Number((safeCount *
|
|
127
|
-
const netRevenueUsdc = Number((safeCount *
|
|
138
|
+
const grossRevenueUsdc = Number((safeCount * resolvedUnitPriceUsdc).toFixed(6));
|
|
139
|
+
const netRevenueUsdc = Number((safeCount * resolvedNetPriceUsdc).toFixed(6));
|
|
128
140
|
const effectiveMarginMultiplier =
|
|
129
141
|
actualCostPerEventUsdc > 0
|
|
130
|
-
? Number((
|
|
142
|
+
? Number((resolvedNetPriceUsdc / actualCostPerEventUsdc).toFixed(2))
|
|
131
143
|
: configuredFloorMultiplier;
|
|
132
144
|
|
|
133
145
|
const withinCostCeiling = actualCostPerEventUsdc <= ACTOR_PRICING_CONTRACT.costCeilingUsdc;
|
|
@@ -142,7 +154,7 @@ export function computeRunEconomicsTelemetry({
|
|
|
142
154
|
durationMs: safeDurationMs,
|
|
143
155
|
memoryMb: safeMemoryMb,
|
|
144
156
|
requestCount: safeRequests,
|
|
145
|
-
actorStartFeeUsdc,
|
|
157
|
+
actorStartFeeUsdc: resolvedStartFeeUsdc,
|
|
146
158
|
totalRunCostUsdc,
|
|
147
159
|
actualCostPerEventUsdc,
|
|
148
160
|
costCeilingUsdc: ACTOR_PRICING_CONTRACT.costCeilingUsdc,
|
|
@@ -203,9 +215,25 @@ export async function loadApifyActorInput(
|
|
|
203
215
|
return JSON.parse(raw);
|
|
204
216
|
}
|
|
205
217
|
|
|
218
|
+
const isAtHome = env?.APIFY_IS_AT_HOME === '1' || env?.APIFY_IS_AT_HOME === 'true';
|
|
219
|
+
const applyAtHomeSchemaDefaults = (parsedInput) => {
|
|
220
|
+
if (
|
|
221
|
+
isAtHome &&
|
|
222
|
+
parsedInput &&
|
|
223
|
+
typeof parsedInput === 'object' &&
|
|
224
|
+
!Array.isArray(parsedInput) &&
|
|
225
|
+
!Object.prototype.hasOwnProperty.call(parsedInput, 'query') &&
|
|
226
|
+
!Object.prototype.hasOwnProperty.call(parsedInput, 'advertiserId') &&
|
|
227
|
+
!Object.prototype.hasOwnProperty.call(parsedInput, 'pageToken')
|
|
228
|
+
) {
|
|
229
|
+
return { query: 'Stripe', ...parsedInput };
|
|
230
|
+
}
|
|
231
|
+
return parsedInput;
|
|
232
|
+
};
|
|
233
|
+
|
|
206
234
|
if (env?.APIFY_INPUT_PATH && fs.existsSync(env.APIFY_INPUT_PATH)) {
|
|
207
235
|
const raw = fs.readFileSync(env.APIFY_INPUT_PATH, 'utf8');
|
|
208
|
-
return JSON.parse(raw);
|
|
236
|
+
return applyAtHomeSchemaDefaults(raw && raw.trim() ? JSON.parse(raw) : {});
|
|
209
237
|
}
|
|
210
238
|
|
|
211
239
|
const localStorageDir =
|
|
@@ -224,7 +252,8 @@ export async function loadApifyActorInput(
|
|
|
224
252
|
candidateName
|
|
225
253
|
);
|
|
226
254
|
if (fs.existsSync(localInputPath)) {
|
|
227
|
-
|
|
255
|
+
const raw = fs.readFileSync(localInputPath, 'utf8');
|
|
256
|
+
return applyAtHomeSchemaDefaults(raw && raw.trim() ? JSON.parse(raw) : {});
|
|
228
257
|
}
|
|
229
258
|
}
|
|
230
259
|
}
|
|
@@ -254,9 +283,11 @@ export async function loadApifyActorInput(
|
|
|
254
283
|
});
|
|
255
284
|
if (res.status === 200) {
|
|
256
285
|
const text = await res.text();
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
286
|
+
const parsed = text && text.trim() ? JSON.parse(text) : {};
|
|
287
|
+
return applyAtHomeSchemaDefaults(parsed);
|
|
288
|
+
}
|
|
289
|
+
if (res.status === 204 || res.status === 404) {
|
|
290
|
+
return applyAtHomeSchemaDefaults({});
|
|
260
291
|
}
|
|
261
292
|
} finally {
|
|
262
293
|
clearTimeout(timer);
|
|
@@ -310,6 +341,19 @@ export function createApifyPlatformHooks(env = process.env, fetchImpl = globalTh
|
|
|
310
341
|
env?.APIFY_API_BASE_URL,
|
|
311
342
|
env?.APIFY_API_PUBLIC_BASE_URL
|
|
312
343
|
);
|
|
344
|
+
let pricingInfoChargeEvents = null;
|
|
345
|
+
const rawPricingInfoEnv = env?.APIFY_ACTOR_PRICING_INFO || env?.ACTOR_PRICING_INFO;
|
|
346
|
+
if (rawPricingInfoEnv) {
|
|
347
|
+
try {
|
|
348
|
+
const parsedPricing = JSON.parse(String(rawPricingInfoEnv));
|
|
349
|
+
const eventsObj = parsedPricing?.pricingPerEvent?.actorChargeEvents;
|
|
350
|
+
if (eventsObj && typeof eventsObj === 'object') {
|
|
351
|
+
pricingInfoChargeEvents = eventsObj;
|
|
352
|
+
}
|
|
353
|
+
} catch {
|
|
354
|
+
pricingInfoChargeEvents = null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
313
357
|
const rawMaxChargeStr =
|
|
314
358
|
env?.ACTOR_MAX_TOTAL_CHARGE_USD !== undefined && env?.ACTOR_MAX_TOTAL_CHARGE_USD !== null
|
|
315
359
|
? String(env.ACTOR_MAX_TOTAL_CHARGE_USD).trim()
|
|
@@ -320,23 +364,28 @@ export function createApifyPlatformHooks(env = process.env, fetchImpl = globalTh
|
|
|
320
364
|
.trim()
|
|
321
365
|
.toLowerCase();
|
|
322
366
|
const includeStartFee = includeStartFeeFlag !== '0' && includeStartFeeFlag !== 'false';
|
|
367
|
+
const pricingInfoStartFeeUsd = Number(
|
|
368
|
+
pricingInfoChargeEvents?.['apify-actor-start']?.eventPriceUsd
|
|
369
|
+
);
|
|
323
370
|
const explicitStartFeeUsd =
|
|
324
371
|
env?.ACTOR_START_FEE_USD !== undefined &&
|
|
325
372
|
env?.ACTOR_START_FEE_USD !== null &&
|
|
326
373
|
String(env.ACTOR_START_FEE_USD).trim() !== ''
|
|
327
374
|
? Number(env.ACTOR_START_FEE_USD)
|
|
328
|
-
:
|
|
375
|
+
: Number.isFinite(pricingInfoStartFeeUsd) && pricingInfoStartFeeUsd >= 0
|
|
376
|
+
? pricingInfoStartFeeUsd
|
|
377
|
+
: ACTOR_PRICING_CONTRACT.actorStartFeeUsdc;
|
|
329
378
|
const startFeeMicros =
|
|
330
379
|
includeStartFee && Number.isFinite(explicitStartFeeUsd) && explicitStartFeeUsd >= 0
|
|
331
380
|
? Math.round(explicitStartFeeUsd * 1_000_000)
|
|
332
381
|
: 0;
|
|
333
|
-
const unitPriceMicros = Math.round(ACTOR_PRICING_CONTRACT.priceUsdc * 1_000_000);
|
|
334
382
|
const maxBudgetMicros = hasMaxChargeUsd ? Math.round(maxTotalChargeUsdRaw * 1_000_000) : 0;
|
|
335
|
-
const maxPayableEvents = hasMaxChargeUsd
|
|
336
|
-
? Math.floor(Math.max(0, maxBudgetMicros - startFeeMicros) / unitPriceMicros)
|
|
337
|
-
: Infinity;
|
|
338
383
|
let cumulativeChargedInAdapter = 0;
|
|
339
384
|
let adapterChargeLimitLatched = false;
|
|
385
|
+
hooks.actorStartFeeUsdc =
|
|
386
|
+
includeStartFee && Number.isFinite(explicitStartFeeUsd) && explicitStartFeeUsd >= 0
|
|
387
|
+
? explicitStartFeeUsd
|
|
388
|
+
: 0;
|
|
340
389
|
|
|
341
390
|
if (kvsId) {
|
|
342
391
|
const checkpointRecordUrl = `${apiBase}/v2/key-value-stores/${encodeURIComponent(kvsId)}/records/${ACTOR_PRICING_CONTRACT.checkpointKey}`;
|
|
@@ -470,15 +519,80 @@ export function createApifyPlatformHooks(env = process.env, fetchImpl = globalTh
|
|
|
470
519
|
|
|
471
520
|
if (runId) {
|
|
472
521
|
const chargeUrl = `${apiBase}/v2/actor-runs/${encodeURIComponent(runId)}/charge`;
|
|
522
|
+
const allowUnbilledAtHome = env?.ADPULSE_ALLOW_UNBILLED_AT_HOME === '1';
|
|
523
|
+
let unbilledOwnerTestLatched = false;
|
|
524
|
+
let latchedPlatformEventName = null;
|
|
525
|
+
const resolvePlatformEventAndPrice = (rawEventName) => {
|
|
526
|
+
const candidate = String(rawEventName || ACTOR_PRICING_CONTRACT.eventName);
|
|
527
|
+
const effectiveName =
|
|
528
|
+
latchedPlatformEventName ||
|
|
529
|
+
(() => {
|
|
530
|
+
if (pricingInfoChargeEvents) {
|
|
531
|
+
if (Object.prototype.hasOwnProperty.call(pricingInfoChargeEvents, candidate)) {
|
|
532
|
+
return candidate;
|
|
533
|
+
}
|
|
534
|
+
if (
|
|
535
|
+
candidate === 'ad-creative-scraped' ||
|
|
536
|
+
candidate === 'ad_transparency_creative_retrieved'
|
|
537
|
+
) {
|
|
538
|
+
if (
|
|
539
|
+
Object.prototype.hasOwnProperty.call(
|
|
540
|
+
pricingInfoChargeEvents,
|
|
541
|
+
'ad_transparency_creative_retrieved'
|
|
542
|
+
)
|
|
543
|
+
) {
|
|
544
|
+
return 'ad_transparency_creative_retrieved';
|
|
545
|
+
}
|
|
546
|
+
if (
|
|
547
|
+
Object.prototype.hasOwnProperty.call(
|
|
548
|
+
pricingInfoChargeEvents,
|
|
549
|
+
'ad-creative-scraped'
|
|
550
|
+
)
|
|
551
|
+
) {
|
|
552
|
+
return 'ad-creative-scraped';
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return candidate;
|
|
557
|
+
})();
|
|
558
|
+
const rawEventPrice = Number(pricingInfoChargeEvents?.[effectiveName]?.eventPriceUsd);
|
|
559
|
+
const unitPriceUsdc =
|
|
560
|
+
Number.isFinite(rawEventPrice) && rawEventPrice > 0
|
|
561
|
+
? rawEventPrice
|
|
562
|
+
: ACTOR_PRICING_CONTRACT.priceUsdc;
|
|
563
|
+
return { eventName: effectiveName, unitPriceUsdc };
|
|
564
|
+
};
|
|
565
|
+
hooks.unitPriceUsdc = resolvePlatformEventAndPrice(
|
|
566
|
+
ACTOR_PRICING_CONTRACT.eventName
|
|
567
|
+
).unitPriceUsdc;
|
|
473
568
|
hooks.chargeEvent = async (receipt) => {
|
|
474
569
|
const requestedCount = Math.max(0, Number(receipt?.count) || 0);
|
|
570
|
+
const { eventName: primaryEventName, unitPriceUsdc: resolvedUnitPriceUsdc } =
|
|
571
|
+
resolvePlatformEventAndPrice(receipt?.eventName);
|
|
572
|
+
hooks.unitPriceUsdc = resolvedUnitPriceUsdc;
|
|
573
|
+
if (unbilledOwnerTestLatched) {
|
|
574
|
+
return {
|
|
575
|
+
chargedCount: 0,
|
|
576
|
+
eventChargeLimitReached: false,
|
|
577
|
+
unbilledOwnerTestBypass: true,
|
|
578
|
+
chargedCreativeIds: [],
|
|
579
|
+
eventName: primaryEventName,
|
|
580
|
+
unitPriceUsdc: resolvedUnitPriceUsdc,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
475
583
|
if (adapterChargeLimitLatched) {
|
|
476
584
|
return {
|
|
477
585
|
chargedCount: 0,
|
|
478
586
|
eventChargeLimitReached: true,
|
|
479
587
|
chargedCreativeIds: [],
|
|
588
|
+
eventName: primaryEventName,
|
|
589
|
+
unitPriceUsdc: resolvedUnitPriceUsdc,
|
|
480
590
|
};
|
|
481
591
|
}
|
|
592
|
+
const unitPriceMicros = Math.round(resolvedUnitPriceUsdc * 1_000_000);
|
|
593
|
+
const maxPayableEvents = hasMaxChargeUsd
|
|
594
|
+
? Math.floor(Math.max(0, maxBudgetMicros - startFeeMicros) / unitPriceMicros)
|
|
595
|
+
: Infinity;
|
|
482
596
|
const remainingAllowance = Number.isFinite(maxPayableEvents)
|
|
483
597
|
? Math.max(0, maxPayableEvents - cumulativeChargedInAdapter)
|
|
484
598
|
: requestedCount;
|
|
@@ -493,25 +607,65 @@ export function createApifyPlatformHooks(env = process.env, fetchImpl = globalTh
|
|
|
493
607
|
chargedCount: 0,
|
|
494
608
|
eventChargeLimitReached: Number.isFinite(maxPayableEvents),
|
|
495
609
|
chargedCreativeIds: [],
|
|
610
|
+
eventName: primaryEventName,
|
|
611
|
+
unitPriceUsdc: resolvedUnitPriceUsdc,
|
|
496
612
|
};
|
|
497
613
|
}
|
|
498
614
|
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
try {
|
|
502
|
-
const res = await fetchImpl(chargeUrl, {
|
|
615
|
+
const postChargeRequest = async (eventNameToPost, idempotencyKeyToPost, signal) =>
|
|
616
|
+
fetchImpl(chargeUrl, {
|
|
503
617
|
method: 'POST',
|
|
504
618
|
headers: {
|
|
505
619
|
authorization: `Bearer ${token}`,
|
|
506
620
|
'content-type': 'application/json; charset=utf-8',
|
|
507
|
-
'idempotency-key':
|
|
621
|
+
'idempotency-key': idempotencyKeyToPost,
|
|
508
622
|
},
|
|
509
623
|
body: JSON.stringify({
|
|
510
|
-
eventName:
|
|
624
|
+
eventName: eventNameToPost,
|
|
511
625
|
count: countToCharge,
|
|
512
626
|
}),
|
|
513
|
-
signal
|
|
627
|
+
signal,
|
|
514
628
|
});
|
|
629
|
+
|
|
630
|
+
const controller = new AbortController();
|
|
631
|
+
const timer = setTimeout(() => controller.abort(), 600);
|
|
632
|
+
try {
|
|
633
|
+
let effectiveEventName = primaryEventName;
|
|
634
|
+
let effectiveIdempotencyKey = receipt.idempotencyKey;
|
|
635
|
+
let res = await postChargeRequest(
|
|
636
|
+
primaryEventName,
|
|
637
|
+
receipt.idempotencyKey,
|
|
638
|
+
controller.signal
|
|
639
|
+
);
|
|
640
|
+
if (
|
|
641
|
+
res.status === 404 &&
|
|
642
|
+
!pricingInfoChargeEvents &&
|
|
643
|
+
!latchedPlatformEventName &&
|
|
644
|
+
(primaryEventName === 'ad-creative-scraped' ||
|
|
645
|
+
primaryEventName === 'ad_transparency_creative_retrieved')
|
|
646
|
+
) {
|
|
647
|
+
const aliasEventName =
|
|
648
|
+
primaryEventName === 'ad-creative-scraped'
|
|
649
|
+
? 'ad_transparency_creative_retrieved'
|
|
650
|
+
: 'ad-creative-scraped';
|
|
651
|
+
const aliasIdempotencyKey = `${receipt.idempotencyKey}-${aliasEventName}`;
|
|
652
|
+
const fallbackRes = await postChargeRequest(
|
|
653
|
+
aliasEventName,
|
|
654
|
+
aliasIdempotencyKey,
|
|
655
|
+
controller.signal
|
|
656
|
+
);
|
|
657
|
+
if (fallbackRes.status !== 404) {
|
|
658
|
+
res = fallbackRes;
|
|
659
|
+
effectiveEventName = aliasEventName;
|
|
660
|
+
effectiveIdempotencyKey = aliasIdempotencyKey;
|
|
661
|
+
if (
|
|
662
|
+
(fallbackRes.status >= 200 && fallbackRes.status < 300) ||
|
|
663
|
+
fallbackRes.status === 402
|
|
664
|
+
) {
|
|
665
|
+
latchedPlatformEventName = aliasEventName;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
515
669
|
if (res.status === 402) {
|
|
516
670
|
// Platform enforced spending ceiling (HTTP 402 Payment Required): latch so no further charge POSTs occur
|
|
517
671
|
adapterChargeLimitLatched = true;
|
|
@@ -519,10 +673,50 @@ export function createApifyPlatformHooks(env = process.env, fetchImpl = globalTh
|
|
|
519
673
|
chargedCount: 0,
|
|
520
674
|
eventChargeLimitReached: true,
|
|
521
675
|
chargedCreativeIds: [],
|
|
676
|
+
eventName: effectiveEventName,
|
|
677
|
+
unitPriceUsdc: resolvedUnitPriceUsdc,
|
|
678
|
+
idempotencyKey: effectiveIdempotencyKey,
|
|
522
679
|
};
|
|
523
680
|
}
|
|
681
|
+
if (allowUnbilledAtHome && (res.status === 405 || res.status === 403)) {
|
|
682
|
+
let isUnmonetizedNotAllowed = res.status === 405;
|
|
683
|
+
if (res.status === 403) {
|
|
684
|
+
try {
|
|
685
|
+
const errText = await res.text();
|
|
686
|
+
const errJson = errText && errText.trim() ? JSON.parse(errText) : null;
|
|
687
|
+
isUnmonetizedNotAllowed = errJson?.error?.type === 'operation-not-allowed';
|
|
688
|
+
} catch {
|
|
689
|
+
isUnmonetizedNotAllowed = false;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (isUnmonetizedNotAllowed) {
|
|
693
|
+
unbilledOwnerTestLatched = true;
|
|
694
|
+
return {
|
|
695
|
+
chargedCount: 0,
|
|
696
|
+
eventChargeLimitReached: false,
|
|
697
|
+
unbilledOwnerTestBypass: true,
|
|
698
|
+
chargedCreativeIds: [],
|
|
699
|
+
eventName: effectiveEventName,
|
|
700
|
+
unitPriceUsdc: resolvedUnitPriceUsdc,
|
|
701
|
+
idempotencyKey: effectiveIdempotencyKey,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
}
|
|
524
705
|
if (res.status < 200 || res.status >= 300) {
|
|
525
|
-
|
|
706
|
+
let errSuffix = '';
|
|
707
|
+
try {
|
|
708
|
+
const errText = await res.text();
|
|
709
|
+
if (errText && errText.trim()) {
|
|
710
|
+
const errJson = JSON.parse(errText);
|
|
711
|
+
const errType = errJson?.error?.type;
|
|
712
|
+
if (typeof errType === 'string' && /^[A-Za-z0-9_.:-]{1,64}$/.test(errType)) {
|
|
713
|
+
errSuffix = ` (${errType})`;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
} catch {
|
|
717
|
+
errSuffix = '';
|
|
718
|
+
}
|
|
719
|
+
throw new Error(`Apify Pay-per-Event charge HTTP ${res.status}${errSuffix}`);
|
|
526
720
|
}
|
|
527
721
|
const rawText = await res.text();
|
|
528
722
|
const parsed = rawText && rawText.trim() ? JSON.parse(rawText) : {};
|
|
@@ -552,14 +746,32 @@ export function createApifyPlatformHooks(env = process.env, fetchImpl = globalTh
|
|
|
552
746
|
Boolean(dataObj?.eventChargeLimitReached) ||
|
|
553
747
|
limitReached ||
|
|
554
748
|
(Number.isFinite(maxPayableEvents) && cumulativeChargedInAdapter >= maxPayableEvents);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
749
|
+
let subsetIds;
|
|
750
|
+
if (Array.isArray(dataObj?.chargedCreativeIds)) {
|
|
751
|
+
const reqIds = Array.isArray(receipt?.creativeIds) ? receipt.creativeIds : null;
|
|
752
|
+
const uniqueValidIds = new Set(
|
|
753
|
+
dataObj.chargedCreativeIds.filter(
|
|
754
|
+
(id) => typeof id === 'string' && (!reqIds || reqIds.includes(id))
|
|
755
|
+
)
|
|
756
|
+
);
|
|
757
|
+
if (
|
|
758
|
+
dataObj.chargedCreativeIds.length !== resolvedChargedCount ||
|
|
759
|
+
uniqueValidIds.size !== resolvedChargedCount
|
|
760
|
+
) {
|
|
761
|
+
throw new Error(
|
|
762
|
+
'Apify Pay-per-Event charge response contained invalid chargedCreativeIds'
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
subsetIds = Array.from(uniqueValidIds);
|
|
766
|
+
} else if (Array.isArray(receipt?.creativeIds)) {
|
|
767
|
+
subsetIds = receipt.creativeIds.slice(0, resolvedChargedCount);
|
|
768
|
+
}
|
|
560
769
|
return {
|
|
561
770
|
chargedCount: resolvedChargedCount,
|
|
562
771
|
eventChargeLimitReached: resolvedLimitReached,
|
|
772
|
+
eventName: effectiveEventName,
|
|
773
|
+
unitPriceUsdc: resolvedUnitPriceUsdc,
|
|
774
|
+
idempotencyKey: effectiveIdempotencyKey,
|
|
563
775
|
...(subsetIds ? { chargedCreativeIds: subsetIds } : {}),
|
|
564
776
|
};
|
|
565
777
|
} finally {
|
|
@@ -704,7 +916,7 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
704
916
|
'default_run'
|
|
705
917
|
);
|
|
706
918
|
const hasChargeHook = typeof hooks.chargeEvent === 'function';
|
|
707
|
-
|
|
919
|
+
let billingMode =
|
|
708
920
|
hasChargeHook || hooks.isAtHome === true ? 'APIFY_PAY_PER_EVENT' : 'LOCAL_UNBILLED';
|
|
709
921
|
|
|
710
922
|
const maxRunDurationMs = Math.min(
|
|
@@ -1255,6 +1467,7 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1255
1467
|
advertiserId: targetItem.advertiserId,
|
|
1256
1468
|
creativeId: targetItem.creativeId,
|
|
1257
1469
|
includeAdCopy: Boolean(input?.includeAdCopy),
|
|
1470
|
+
fallbackPreviewUrl: targetItem.previewUrl,
|
|
1258
1471
|
fetchImpl: hooks.fetchImpl,
|
|
1259
1472
|
nowFn: nowMs,
|
|
1260
1473
|
sleepFn,
|
|
@@ -1282,6 +1495,14 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1282
1495
|
rateLimitedInEnrich = true;
|
|
1283
1496
|
}
|
|
1284
1497
|
}
|
|
1498
|
+
} else if (detailRes.ok && detailRes.untrustedAdContent) {
|
|
1499
|
+
targetItem.untrustedAdContent = detailRes.untrustedAdContent;
|
|
1500
|
+
if (
|
|
1501
|
+
detailRes.untrustedAdContent.extractionStatus === 'UPSTREAM_RATE_LIMITED' ||
|
|
1502
|
+
detailRes.untrustedAdContent.contentJsFetchStatus === 'UPSTREAM_RATE_LIMITED'
|
|
1503
|
+
) {
|
|
1504
|
+
rateLimitedInEnrich = true;
|
|
1505
|
+
}
|
|
1285
1506
|
}
|
|
1286
1507
|
}
|
|
1287
1508
|
}
|
|
@@ -1323,6 +1544,8 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1323
1544
|
chargedEventCount: hasChargeHook ? chargedSet.size : 0,
|
|
1324
1545
|
durationMs,
|
|
1325
1546
|
requestCount: Math.max(1, totalRpcRequests),
|
|
1547
|
+
unitPriceUsdc: hooks?.unitPriceUsdc ?? ACTOR_PRICING_CONTRACT.priceUsdc,
|
|
1548
|
+
actorStartFeeUsdc: hooks?.actorStartFeeUsdc,
|
|
1326
1549
|
}),
|
|
1327
1550
|
});
|
|
1328
1551
|
}
|
|
@@ -1346,6 +1569,7 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1346
1569
|
}
|
|
1347
1570
|
};
|
|
1348
1571
|
|
|
1572
|
+
let lastResolvedUnitPriceUsdc = hooks?.unitPriceUsdc ?? ACTOR_PRICING_CONTRACT.priceUsdc;
|
|
1349
1573
|
for (let offset = 0; offset < newBillableCandidates.length; offset += chunkSize) {
|
|
1350
1574
|
const effectiveChunkDeadlineMs = hasChargeHook
|
|
1351
1575
|
? deadlineAtMs - step4ReserveMs
|
|
@@ -1446,6 +1670,28 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1446
1670
|
// Step 4.2: Strict ChargeResult validation (never fallback to ?? unchargedChunk.length)
|
|
1447
1671
|
const chargedCount = chargeResult?.chargedCount;
|
|
1448
1672
|
const eventChargeLimitReached = Boolean(chargeResult?.eventChargeLimitReached);
|
|
1673
|
+
const resolvedReceiptEventName =
|
|
1674
|
+
typeof chargeResult?.eventName === 'string' && chargeResult.eventName
|
|
1675
|
+
? chargeResult.eventName
|
|
1676
|
+
: receipt.eventName;
|
|
1677
|
+
const resolvedReceiptUnitPriceUsdc =
|
|
1678
|
+
Number.isFinite(Number(chargeResult?.unitPriceUsdc)) && Number(chargeResult.unitPriceUsdc) > 0
|
|
1679
|
+
? Number(chargeResult.unitPriceUsdc)
|
|
1680
|
+
: receipt.unitPriceUsdc;
|
|
1681
|
+
lastResolvedUnitPriceUsdc = resolvedReceiptUnitPriceUsdc;
|
|
1682
|
+
const resolvedReceiptIdempotencyKey =
|
|
1683
|
+
typeof chargeResult?.idempotencyKey === 'string' && chargeResult.idempotencyKey
|
|
1684
|
+
? chargeResult.idempotencyKey
|
|
1685
|
+
: receipt.idempotencyKey;
|
|
1686
|
+
const buildFinalReceipt = (actualChargedCount, extra = {}) => ({
|
|
1687
|
+
...receipt,
|
|
1688
|
+
idempotencyKey: resolvedReceiptIdempotencyKey,
|
|
1689
|
+
eventName: resolvedReceiptEventName,
|
|
1690
|
+
unitPriceUsdc: resolvedReceiptUnitPriceUsdc,
|
|
1691
|
+
chunkAmountUsdc: Number((actualChargedCount * resolvedReceiptUnitPriceUsdc).toFixed(6)),
|
|
1692
|
+
chargedCount: actualChargedCount,
|
|
1693
|
+
...extra,
|
|
1694
|
+
});
|
|
1449
1695
|
|
|
1450
1696
|
if (
|
|
1451
1697
|
!chargeResult ||
|
|
@@ -1478,11 +1724,7 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1478
1724
|
|
|
1479
1725
|
const pushRes = await pushDataWithRetry(unchargedChunk);
|
|
1480
1726
|
if (!pushRes.ok) {
|
|
1481
|
-
chargeReceipts.push({
|
|
1482
|
-
...receipt,
|
|
1483
|
-
chargedCount,
|
|
1484
|
-
eventChargeLimitReached,
|
|
1485
|
-
});
|
|
1727
|
+
chargeReceipts.push(buildFinalReceipt(chargedCount, { eventChargeLimitReached }));
|
|
1486
1728
|
finalOk = false;
|
|
1487
1729
|
finalStatus = 'PARTIAL_BILLING_HALT';
|
|
1488
1730
|
haltReason = `Dataset pushData failed after charge (paid items preserved in KVS checkpoint stagedItemsById): ${pushRes.error?.message || String(pushRes.error)}`;
|
|
@@ -1495,11 +1737,7 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1495
1737
|
deliveredCount = deliveredSet.size;
|
|
1496
1738
|
await persistCheckpointPostCharge();
|
|
1497
1739
|
|
|
1498
|
-
chargeReceipts.push({
|
|
1499
|
-
...receipt,
|
|
1500
|
-
chargedCount,
|
|
1501
|
-
eventChargeLimitReached,
|
|
1502
|
-
});
|
|
1740
|
+
chargeReceipts.push(buildFinalReceipt(chargedCount, { eventChargeLimitReached }));
|
|
1503
1741
|
|
|
1504
1742
|
if (eventChargeLimitReached) {
|
|
1505
1743
|
finalStatus = 'CHARGE_LIMIT_REACHED';
|
|
@@ -1507,12 +1745,32 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1507
1745
|
}
|
|
1508
1746
|
} else if (chargedCount === 0) {
|
|
1509
1747
|
inFlightSet = new Set();
|
|
1748
|
+
if (chargeResult?.unbilledOwnerTestBypass === true) {
|
|
1749
|
+
billingMode = 'UNBILLED_OWNER_TEST';
|
|
1750
|
+
await persistCheckpointPostCharge();
|
|
1751
|
+
const pushRes = await pushDataWithRetry(unchargedChunk);
|
|
1752
|
+
if (!pushRes.ok) {
|
|
1753
|
+
finalOk = false;
|
|
1754
|
+
finalStatus = 'PARTIAL_BILLING_HALT';
|
|
1755
|
+
haltReason = `Dataset pushData failed in UNBILLED_OWNER_TEST mode: ${pushRes.error?.message || String(pushRes.error)}`;
|
|
1756
|
+
break;
|
|
1757
|
+
}
|
|
1758
|
+
for (const c of unchargedChunk) {
|
|
1759
|
+
deliveredSet.add(c.creativeId);
|
|
1760
|
+
newlyDeliveredItems.push(c);
|
|
1761
|
+
}
|
|
1762
|
+
deliveredCount = deliveredSet.size;
|
|
1763
|
+
await persistCheckpointPostCharge();
|
|
1764
|
+
chargeReceipts.push(
|
|
1765
|
+
buildFinalReceipt(0, {
|
|
1766
|
+
eventChargeLimitReached: false,
|
|
1767
|
+
unbilledOwnerTestBypass: true,
|
|
1768
|
+
})
|
|
1769
|
+
);
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1510
1772
|
await persistCheckpointPostCharge();
|
|
1511
|
-
chargeReceipts.push({
|
|
1512
|
-
...receipt,
|
|
1513
|
-
chargedCount: 0,
|
|
1514
|
-
eventChargeLimitReached,
|
|
1515
|
-
});
|
|
1773
|
+
chargeReceipts.push(buildFinalReceipt(0, { eventChargeLimitReached }));
|
|
1516
1774
|
if (eventChargeLimitReached) {
|
|
1517
1775
|
finalOk = true;
|
|
1518
1776
|
finalStatus = 'CHARGE_LIMIT_REACHED';
|
|
@@ -1524,11 +1782,19 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1524
1782
|
break;
|
|
1525
1783
|
} else {
|
|
1526
1784
|
// Partial chunk charge (0 < chargedCount < unchargedChunk.length)
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1785
|
+
const hasExplicitIdsArray = Array.isArray(chargeResult.chargedCreativeIds);
|
|
1786
|
+
const explicitChargedIds = hasExplicitIdsArray
|
|
1787
|
+
? Array.from(
|
|
1788
|
+
new Set(chargeResult.chargedCreativeIds.filter((id) => chunkIds.includes(id)))
|
|
1789
|
+
)
|
|
1529
1790
|
: null;
|
|
1791
|
+
const isValidExplicitChargedIds = Boolean(
|
|
1792
|
+
explicitChargedIds &&
|
|
1793
|
+
explicitChargedIds.length === chargedCount &&
|
|
1794
|
+
chargeResult.chargedCreativeIds.length === chargedCount
|
|
1795
|
+
);
|
|
1530
1796
|
|
|
1531
|
-
if (
|
|
1797
|
+
if (isValidExplicitChargedIds) {
|
|
1532
1798
|
for (const id of explicitChargedIds) {
|
|
1533
1799
|
chargedSet.add(id);
|
|
1534
1800
|
}
|
|
@@ -1539,12 +1805,12 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1539
1805
|
const paidSubset = unchargedChunk.filter((c) => explicitChargedIds.includes(c.creativeId));
|
|
1540
1806
|
const pushRes = await pushDataWithRetry(paidSubset);
|
|
1541
1807
|
if (!pushRes.ok) {
|
|
1542
|
-
chargeReceipts.push(
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1808
|
+
chargeReceipts.push(
|
|
1809
|
+
buildFinalReceipt(chargedCount, {
|
|
1810
|
+
chargedCreativeIds: explicitChargedIds,
|
|
1811
|
+
eventChargeLimitReached,
|
|
1812
|
+
})
|
|
1813
|
+
);
|
|
1548
1814
|
finalOk = false;
|
|
1549
1815
|
finalStatus = 'PARTIAL_BILLING_HALT';
|
|
1550
1816
|
haltReason = `Dataset pushData failed after partial charge (paid items preserved in KVS checkpoint stagedItemsById): ${pushRes.error?.message || String(pushRes.error)}`;
|
|
@@ -1557,12 +1823,12 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1557
1823
|
deliveredCount = deliveredSet.size;
|
|
1558
1824
|
await persistCheckpointPostCharge();
|
|
1559
1825
|
|
|
1560
|
-
chargeReceipts.push(
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1826
|
+
chargeReceipts.push(
|
|
1827
|
+
buildFinalReceipt(chargedCount, {
|
|
1828
|
+
chargedCreativeIds: explicitChargedIds,
|
|
1829
|
+
eventChargeLimitReached,
|
|
1830
|
+
})
|
|
1831
|
+
);
|
|
1566
1832
|
|
|
1567
1833
|
finalOk = eventChargeLimitReached;
|
|
1568
1834
|
finalStatus = eventChargeLimitReached ? 'CHARGE_LIMIT_REACHED' : 'PARTIAL_BILLING_HALT';
|
|
@@ -1570,7 +1836,7 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1570
1836
|
haltReason = `Partial chunk charge (${chargedCount}/${unchargedChunk.length}) without charge limit flag`;
|
|
1571
1837
|
}
|
|
1572
1838
|
break;
|
|
1573
|
-
} else if (eventChargeLimitReached) {
|
|
1839
|
+
} else if (!hasExplicitIdsArray && eventChargeLimitReached) {
|
|
1574
1840
|
// Standard Apify SDK ChargeResult when hitting user's max charge ceiling:
|
|
1575
1841
|
// Attribute charge deterministically to the ordered prefix `unchargedChunk.slice(0, chargedCount)`
|
|
1576
1842
|
// so the customer receives every item they paid for, and leave the remaining tail uncharged.
|
|
@@ -1585,12 +1851,12 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1585
1851
|
|
|
1586
1852
|
const pushRes = await pushDataWithRetry(paidPrefix);
|
|
1587
1853
|
if (!pushRes.ok) {
|
|
1588
|
-
chargeReceipts.push(
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1854
|
+
chargeReceipts.push(
|
|
1855
|
+
buildFinalReceipt(chargedCount, {
|
|
1856
|
+
chargedCreativeIds: paidPrefixIds,
|
|
1857
|
+
eventChargeLimitReached: true,
|
|
1858
|
+
})
|
|
1859
|
+
);
|
|
1594
1860
|
finalOk = false;
|
|
1595
1861
|
finalStatus = 'PARTIAL_BILLING_HALT';
|
|
1596
1862
|
haltReason = `Dataset pushData failed after charge limit prefix (paid items preserved in KVS checkpoint stagedItemsById): ${pushRes.error?.message || String(pushRes.error)}`;
|
|
@@ -1603,12 +1869,12 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1603
1869
|
deliveredCount = deliveredSet.size;
|
|
1604
1870
|
await persistCheckpointPostCharge();
|
|
1605
1871
|
|
|
1606
|
-
chargeReceipts.push(
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1872
|
+
chargeReceipts.push(
|
|
1873
|
+
buildFinalReceipt(chargedCount, {
|
|
1874
|
+
chargedCreativeIds: paidPrefixIds,
|
|
1875
|
+
eventChargeLimitReached: true,
|
|
1876
|
+
})
|
|
1877
|
+
);
|
|
1612
1878
|
|
|
1613
1879
|
finalOk = true;
|
|
1614
1880
|
finalStatus = 'CHARGE_LIMIT_REACHED';
|
|
@@ -1623,11 +1889,11 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1623
1889
|
chargedEventCount = chargedSet.size + chargedCount;
|
|
1624
1890
|
await persistCheckpointPostCharge();
|
|
1625
1891
|
|
|
1626
|
-
chargeReceipts.push(
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1892
|
+
chargeReceipts.push(
|
|
1893
|
+
buildFinalReceipt(chargedCount, {
|
|
1894
|
+
eventChargeLimitReached: false,
|
|
1895
|
+
})
|
|
1896
|
+
);
|
|
1631
1897
|
|
|
1632
1898
|
finalOk = false;
|
|
1633
1899
|
finalStatus = 'PARTIAL_BILLING_HALT';
|
|
@@ -1690,6 +1956,8 @@ export async function runGoogleAdsTransparencyActor(input = {}, hooks = {}) {
|
|
|
1690
1956
|
chargedEventCount,
|
|
1691
1957
|
durationMs,
|
|
1692
1958
|
requestCount: Math.max(1, totalRpcRequests),
|
|
1959
|
+
unitPriceUsdc: lastResolvedUnitPriceUsdc,
|
|
1960
|
+
actorStartFeeUsdc: hooks?.actorStartFeeUsdc,
|
|
1693
1961
|
});
|
|
1694
1962
|
|
|
1695
1963
|
return finalizeActorReturn({
|
package/src/mcp_server.mjs
CHANGED
|
@@ -43,7 +43,7 @@ export const ENCODED_BRAND_ICON_DATA_URI = `data:image/svg+xml;utf8,${encodeURIC
|
|
|
43
43
|
export const MCP_SERVER_INFO = Object.freeze({
|
|
44
44
|
name: 'google-ads-transparency-mcp-actor',
|
|
45
45
|
title: 'RevClamp AdPulse - For Google Ads Transparency Center (MCP & Apify Actor)',
|
|
46
|
-
version: '1.0.
|
|
46
|
+
version: '1.0.2',
|
|
47
47
|
description:
|
|
48
48
|
'Find verified Google Ads advertiser entities and extract active or historical ad creatives (Text, Image, Video), preview links, and run dates. Independent intelligence tool by RevClamp; not affiliated with or endorsed by Google LLC.',
|
|
49
49
|
// Forward-compatible MCP Registry & Glama metadata extensions (ignored by older stdio clients)
|
package/src/rpc_client.mjs
CHANGED
|
@@ -1928,7 +1928,7 @@ export async function resolveAdvertiserEntities(options = {}) {
|
|
|
1928
1928
|
const endpointUrl = options.endpointUrl || GOOGLE_ADS_RPC_ENDPOINTS.SearchSuggestions;
|
|
1929
1929
|
const rpcRes = await executeGoogleAdsRpcPost(
|
|
1930
1930
|
endpointUrl,
|
|
1931
|
-
{ '1': searchQueryStr, '2':
|
|
1931
|
+
{ '1': searchQueryStr, '2': 50, '3': 10 },
|
|
1932
1932
|
options
|
|
1933
1933
|
);
|
|
1934
1934
|
|
|
@@ -2291,8 +2291,20 @@ function isAllowedLandingHostname(hostname) {
|
|
|
2291
2291
|
return true;
|
|
2292
2292
|
}
|
|
2293
2293
|
|
|
2294
|
+
const VAST_CDATA_WRAPPER_SUFFIX_RE =
|
|
2295
|
+
/(?:\]\]>|\]\]%3E|%5D%5D%3E|%5D%5D%253E|<\/[A-Za-z0-9_:-]+>|%3C\/[A-Za-z0-9_:-]+%3E|%3C%2F[A-Za-z0-9_:-]+%3E|%253C%2F[A-Za-z0-9_:-]+%253E).*$/i;
|
|
2296
|
+
|
|
2297
|
+
function stripVastXmlCdataArtifacts(rawStr) {
|
|
2298
|
+
if (typeof rawStr !== 'string' || !rawStr) return '';
|
|
2299
|
+
return rawStr
|
|
2300
|
+
.replace(/^(?:<!\[CDATA\[|%3C!%5BCDATA%5B)+/i, '')
|
|
2301
|
+
.replace(VAST_CDATA_WRAPPER_SUFFIX_RE, '')
|
|
2302
|
+
.replace(/[\]>]+$/, '')
|
|
2303
|
+
.trim();
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2294
2306
|
function normalizeCandidateLandingUrl(rawUrl) {
|
|
2295
|
-
const cleaned = sanitizeUntrustedAdText(rawUrl, 2048);
|
|
2307
|
+
const cleaned = stripVastXmlCdataArtifacts(sanitizeUntrustedAdText(rawUrl, 2048));
|
|
2296
2308
|
if (!cleaned) return null;
|
|
2297
2309
|
try {
|
|
2298
2310
|
const parsed = new URL(cleaned);
|
|
@@ -2363,7 +2375,7 @@ function unwrapCandidateUrlOrAdUrlParam(rawUrl) {
|
|
|
2363
2375
|
const adUrlMatch = decodedRaw.match(/[?&]adurl=([^"'\s&\\]+)/i);
|
|
2364
2376
|
if (adUrlMatch && adUrlMatch[1]) {
|
|
2365
2377
|
try {
|
|
2366
|
-
const target = decodeURIComponent(adUrlMatch[1]);
|
|
2378
|
+
const target = decodeURIComponent(stripVastXmlCdataArtifacts(adUrlMatch[1]));
|
|
2367
2379
|
const normalizedTarget = normalizeCandidateLandingUrl(target);
|
|
2368
2380
|
if (normalizedTarget) return normalizedTarget;
|
|
2369
2381
|
} catch {
|
|
@@ -2583,7 +2595,9 @@ export function extractStaticAdCopyFromContentJs(rawJsText) {
|
|
|
2583
2595
|
const adUrlParamMatch = surface.match(/[?&]adurl=(https?%3A%2F%2F[^"'\s&\\]+)/i);
|
|
2584
2596
|
if (adUrlParamMatch && adUrlParamMatch[1]) {
|
|
2585
2597
|
try {
|
|
2586
|
-
landingPageUrl = normalizeCandidateLandingUrl(
|
|
2598
|
+
landingPageUrl = normalizeCandidateLandingUrl(
|
|
2599
|
+
decodeURIComponent(stripVastXmlCdataArtifacts(adUrlParamMatch[1]))
|
|
2600
|
+
);
|
|
2587
2601
|
if (landingPageUrl) break;
|
|
2588
2602
|
} catch {
|
|
2589
2603
|
landingPageUrl = null;
|
|
@@ -3558,17 +3572,33 @@ export async function getCreativeDetailsById(options = {}) {
|
|
|
3558
3572
|
};
|
|
3559
3573
|
}
|
|
3560
3574
|
|
|
3575
|
+
const nowFn = typeof options.nowFn === 'function' ? options.nowFn : Date.now;
|
|
3576
|
+
const fallbackPreviewUrl =
|
|
3577
|
+
typeof options.fallbackPreviewUrl === 'string' && options.fallbackPreviewUrl.trim()
|
|
3578
|
+
? options.fallbackPreviewUrl.trim()
|
|
3579
|
+
: null;
|
|
3580
|
+
|
|
3561
3581
|
if (canary.status === 'EMPTY_RESULT') {
|
|
3582
|
+
let fallbackUntrustedAdContent = null;
|
|
3583
|
+
if (
|
|
3584
|
+
options.includeAdCopy === true &&
|
|
3585
|
+
fallbackPreviewUrl &&
|
|
3586
|
+
(!options.deadlineAtMs || nowFn() + 200 < options.deadlineAtMs)
|
|
3587
|
+
) {
|
|
3588
|
+
fallbackUntrustedAdContent = await fetchAndExtractStaticAdCopy(fallbackPreviewUrl, options);
|
|
3589
|
+
}
|
|
3562
3590
|
return {
|
|
3563
3591
|
ok: true,
|
|
3564
3592
|
status: 'EMPTY_RESULT',
|
|
3565
3593
|
detail: null,
|
|
3594
|
+
...(fallbackUntrustedAdContent ? { untrustedAdContent: fallbackUntrustedAdContent } : {}),
|
|
3566
3595
|
rpcAttempts: rpcRes.attempts,
|
|
3567
3596
|
};
|
|
3568
3597
|
}
|
|
3569
3598
|
|
|
3570
3599
|
const detailObj = rpcRes.data['1'];
|
|
3571
3600
|
const { previewUrl, archiveImageUrls } = extractCreativeAssetUrls(detailObj?.['5'] ?? detailObj);
|
|
3601
|
+
const effectivePreviewUrl = previewUrl || fallbackPreviewUrl;
|
|
3572
3602
|
const rawRegions = Array.isArray(detailObj?.['17']) ? detailObj['17'] : [];
|
|
3573
3603
|
const regionalPresence = rawRegions
|
|
3574
3604
|
.filter((r) => r && typeof r === 'object')
|
|
@@ -3588,15 +3618,14 @@ export async function getCreativeDetailsById(options = {}) {
|
|
|
3588
3618
|
let untrustedAdContent = extractStaticAdCopyFromContentJs(
|
|
3589
3619
|
JSON.stringify(detailObj?.['5'] ?? detailObj)
|
|
3590
3620
|
);
|
|
3591
|
-
const nowFn = typeof options.nowFn === 'function' ? options.nowFn : Date.now;
|
|
3592
3621
|
if (
|
|
3593
3622
|
!untrustedAdContent.headline &&
|
|
3594
3623
|
!untrustedAdContent.bodyText &&
|
|
3595
3624
|
options.includeAdCopy === true &&
|
|
3596
|
-
|
|
3625
|
+
effectivePreviewUrl &&
|
|
3597
3626
|
(!options.deadlineAtMs || nowFn() + 200 < options.deadlineAtMs)
|
|
3598
3627
|
) {
|
|
3599
|
-
const fetchedCopy = await fetchAndExtractStaticAdCopy(
|
|
3628
|
+
const fetchedCopy = await fetchAndExtractStaticAdCopy(effectivePreviewUrl, options);
|
|
3600
3629
|
if (fetchedCopy.extractionStatus === 'EXTRACTED') {
|
|
3601
3630
|
untrustedAdContent = {
|
|
3602
3631
|
...fetchedCopy,
|