@saltcorn/meta-marketing-api 0.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/LICENSE +21 -0
- package/README.md +180 -0
- package/api.js +561 -0
- package/common.js +449 -0
- package/index.js +516 -0
- package/package.json +36 -0
- package/sync-action.js +337 -0
- package/table-provider.js +351 -0
- package/tests/api.test.js +94 -0
- package/tests/common.test.js +186 -0
package/api.js
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
const fetch = require("node-fetch");
|
|
2
|
+
const crypto = require("crypto");
|
|
3
|
+
|
|
4
|
+
const GRAPH_HOST = "https://graph.facebook.com";
|
|
5
|
+
|
|
6
|
+
// The Graph/Marketing API version used when none is set in the plugin
|
|
7
|
+
// configuration. Meta releases a new version roughly every three months and
|
|
8
|
+
// keeps each one alive for about two years.
|
|
9
|
+
const DEFAULT_API_VERSION = "v26.0";
|
|
10
|
+
|
|
11
|
+
// Meta's Graph API only returns the object id unless you ask for more, so
|
|
12
|
+
// every read below has a sensible default set of fields.
|
|
13
|
+
const DEFAULT_FIELDS = {
|
|
14
|
+
adaccount: [
|
|
15
|
+
"id",
|
|
16
|
+
"account_id",
|
|
17
|
+
"name",
|
|
18
|
+
"account_status",
|
|
19
|
+
"currency",
|
|
20
|
+
"timezone_name",
|
|
21
|
+
"amount_spent",
|
|
22
|
+
"balance",
|
|
23
|
+
"spend_cap",
|
|
24
|
+
"business_name",
|
|
25
|
+
"created_time",
|
|
26
|
+
],
|
|
27
|
+
business: ["id", "name", "created_time", "verification_status"],
|
|
28
|
+
campaign: [
|
|
29
|
+
"id",
|
|
30
|
+
"account_id",
|
|
31
|
+
"name",
|
|
32
|
+
"status",
|
|
33
|
+
"effective_status",
|
|
34
|
+
"configured_status",
|
|
35
|
+
"objective",
|
|
36
|
+
"buying_type",
|
|
37
|
+
"bid_strategy",
|
|
38
|
+
"daily_budget",
|
|
39
|
+
"lifetime_budget",
|
|
40
|
+
"budget_remaining",
|
|
41
|
+
"start_time",
|
|
42
|
+
"stop_time",
|
|
43
|
+
"created_time",
|
|
44
|
+
"updated_time",
|
|
45
|
+
],
|
|
46
|
+
adset: [
|
|
47
|
+
"id",
|
|
48
|
+
"account_id",
|
|
49
|
+
"campaign_id",
|
|
50
|
+
"name",
|
|
51
|
+
"status",
|
|
52
|
+
"effective_status",
|
|
53
|
+
"configured_status",
|
|
54
|
+
"optimization_goal",
|
|
55
|
+
"billing_event",
|
|
56
|
+
"bid_amount",
|
|
57
|
+
"bid_strategy",
|
|
58
|
+
"daily_budget",
|
|
59
|
+
"lifetime_budget",
|
|
60
|
+
"budget_remaining",
|
|
61
|
+
"start_time",
|
|
62
|
+
"end_time",
|
|
63
|
+
"created_time",
|
|
64
|
+
"updated_time",
|
|
65
|
+
],
|
|
66
|
+
ad: [
|
|
67
|
+
"id",
|
|
68
|
+
"account_id",
|
|
69
|
+
"campaign_id",
|
|
70
|
+
"adset_id",
|
|
71
|
+
"name",
|
|
72
|
+
"status",
|
|
73
|
+
"effective_status",
|
|
74
|
+
"configured_status",
|
|
75
|
+
"bid_amount",
|
|
76
|
+
"preview_shareable_link",
|
|
77
|
+
"creative{id,name,thumbnail_url}",
|
|
78
|
+
"created_time",
|
|
79
|
+
"updated_time",
|
|
80
|
+
],
|
|
81
|
+
adcreative: [
|
|
82
|
+
"id",
|
|
83
|
+
"account_id",
|
|
84
|
+
"name",
|
|
85
|
+
"status",
|
|
86
|
+
"title",
|
|
87
|
+
"body",
|
|
88
|
+
"image_url",
|
|
89
|
+
"thumbnail_url",
|
|
90
|
+
"link_url",
|
|
91
|
+
"call_to_action_type",
|
|
92
|
+
"effective_object_story_id",
|
|
93
|
+
"object_story_spec",
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Metrics that are valid at every insights level.
|
|
98
|
+
const BASE_INSIGHTS_FIELDS = [
|
|
99
|
+
"impressions",
|
|
100
|
+
"reach",
|
|
101
|
+
"frequency",
|
|
102
|
+
"clicks",
|
|
103
|
+
"ctr",
|
|
104
|
+
"cpc",
|
|
105
|
+
"cpm",
|
|
106
|
+
"spend",
|
|
107
|
+
"actions",
|
|
108
|
+
"action_values",
|
|
109
|
+
"date_start",
|
|
110
|
+
"date_stop",
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
// Naming the level adds the dimension columns that are valid at that level.
|
|
114
|
+
const INSIGHTS_LEVEL_FIELDS = {
|
|
115
|
+
account: ["account_id", "account_name"],
|
|
116
|
+
campaign: ["account_id", "account_name", "campaign_id", "campaign_name"],
|
|
117
|
+
adset: [
|
|
118
|
+
"account_id",
|
|
119
|
+
"account_name",
|
|
120
|
+
"campaign_id",
|
|
121
|
+
"campaign_name",
|
|
122
|
+
"adset_id",
|
|
123
|
+
"adset_name",
|
|
124
|
+
],
|
|
125
|
+
ad: [
|
|
126
|
+
"account_id",
|
|
127
|
+
"account_name",
|
|
128
|
+
"campaign_id",
|
|
129
|
+
"campaign_name",
|
|
130
|
+
"adset_id",
|
|
131
|
+
"adset_name",
|
|
132
|
+
"ad_id",
|
|
133
|
+
"ad_name",
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Insights columns that are returned as strings but are really numbers.
|
|
138
|
+
const NUMERIC_INSIGHTS_FIELDS = new Set([
|
|
139
|
+
"impressions",
|
|
140
|
+
"reach",
|
|
141
|
+
"frequency",
|
|
142
|
+
"clicks",
|
|
143
|
+
"unique_clicks",
|
|
144
|
+
"inline_link_clicks",
|
|
145
|
+
"ctr",
|
|
146
|
+
"unique_ctr",
|
|
147
|
+
"inline_link_click_ctr",
|
|
148
|
+
"cpc",
|
|
149
|
+
"cpm",
|
|
150
|
+
"cpp",
|
|
151
|
+
"cost_per_inline_link_click",
|
|
152
|
+
"spend",
|
|
153
|
+
"social_spend",
|
|
154
|
+
"objective_results",
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
const insightsFields = (level) => [
|
|
158
|
+
...(INSIGHTS_LEVEL_FIELDS[level] || []),
|
|
159
|
+
...BASE_INSIGHTS_FIELDS,
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Encode one value for the Graph API query string. Arrays of scalars become
|
|
164
|
+
* comma separated lists (fields, breakdowns); anything else structured -
|
|
165
|
+
* time_range, filtering - becomes JSON, which is what Meta expects.
|
|
166
|
+
*/
|
|
167
|
+
const queryValue = (v) => {
|
|
168
|
+
if (v === null || typeof v === "undefined") return null;
|
|
169
|
+
if (Array.isArray(v))
|
|
170
|
+
return v.every((e) => typeof e === "string" || typeof e === "number")
|
|
171
|
+
? v.join(",")
|
|
172
|
+
: JSON.stringify(v);
|
|
173
|
+
if (v instanceof Date) return v.toISOString();
|
|
174
|
+
if (typeof v === "object") return JSON.stringify(v);
|
|
175
|
+
if (typeof v === "boolean") return v ? "true" : "false";
|
|
176
|
+
return String(v);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const toQueryString = (q) =>
|
|
180
|
+
Object.entries(q || {})
|
|
181
|
+
.map(([k, v]) => [k, queryValue(v)])
|
|
182
|
+
.filter(([k, v]) => v !== null && v !== "")
|
|
183
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
184
|
+
.join("&");
|
|
185
|
+
|
|
186
|
+
/** Ad account ids are addressed as act_<id> in the Graph API */
|
|
187
|
+
const actId = (accountId) => {
|
|
188
|
+
const s = `${accountId}`.trim();
|
|
189
|
+
return s.startsWith("act_") ? s : `act_${s}`;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const appSecretProof = (access_token, app_secret) =>
|
|
193
|
+
crypto.createHmac("sha256", app_secret).update(access_token).digest("hex");
|
|
194
|
+
|
|
195
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
196
|
+
|
|
197
|
+
// Error codes worth trying again: transient failures and the various
|
|
198
|
+
// rate limit / business use case throttling codes.
|
|
199
|
+
const TRANSIENT_ERROR_CODES = new Set([1, 2, 4, 17, 32, 341, 613]);
|
|
200
|
+
|
|
201
|
+
const isTransientError = (error) => {
|
|
202
|
+
const code = +error?.code;
|
|
203
|
+
if (TRANSIENT_ERROR_CODES.has(code)) return true;
|
|
204
|
+
return code >= 80000 && code <= 80014;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const mkApiError = (error, status) => {
|
|
208
|
+
const bits = [`code ${error?.code}`];
|
|
209
|
+
if (error?.error_subcode) bits.push(`subcode ${error.error_subcode}`);
|
|
210
|
+
if (error?.fbtrace_id) bits.push(`fbtrace_id ${error.fbtrace_id}`);
|
|
211
|
+
const e = new Error(
|
|
212
|
+
`Meta Marketing API error (${bits.join(", ")}): ${
|
|
213
|
+
error?.error_user_msg || error?.message || "unknown error"
|
|
214
|
+
}`
|
|
215
|
+
);
|
|
216
|
+
e.metaError = error;
|
|
217
|
+
e.status = status;
|
|
218
|
+
return e;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const apiUrl = (path, cfg) => {
|
|
222
|
+
if (/^https?:\/\//.test(path)) return path;
|
|
223
|
+
const version = cfg?.api_version || DEFAULT_API_VERSION;
|
|
224
|
+
return `${GRAPH_HOST}/${version}${path.startsWith("/") ? path : `/${path}`}`;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The single point through which every request to Meta goes.
|
|
229
|
+
*
|
|
230
|
+
* cfg: { access_token, app_id, app_secret, api_version, use_appsecret_proof,
|
|
231
|
+
* max_retries, log_requests }
|
|
232
|
+
* opts: { method, query, noAuth }
|
|
233
|
+
*/
|
|
234
|
+
const graphFetch = async (path, opts = {}, cfg = {}) => {
|
|
235
|
+
const { method = "GET", query, noAuth } = opts;
|
|
236
|
+
const url = apiUrl(path, cfg);
|
|
237
|
+
const params = { ...(query || {}) };
|
|
238
|
+
if (
|
|
239
|
+
!noAuth &&
|
|
240
|
+
cfg?.use_appsecret_proof &&
|
|
241
|
+
cfg?.app_secret &&
|
|
242
|
+
cfg?.access_token &&
|
|
243
|
+
!url.includes("appsecret_proof=")
|
|
244
|
+
)
|
|
245
|
+
params.appsecret_proof = appSecretProof(cfg.access_token, cfg.app_secret);
|
|
246
|
+
|
|
247
|
+
const qs = toQueryString(params);
|
|
248
|
+
const fullUrl = qs ? `${url}${url.includes("?") ? "&" : "?"}${qs}` : url;
|
|
249
|
+
|
|
250
|
+
const headers = { Accept: "application/json" };
|
|
251
|
+
if (!noAuth && cfg?.access_token)
|
|
252
|
+
headers.Authorization = `Bearer ${cfg.access_token}`;
|
|
253
|
+
|
|
254
|
+
const maxRetries =
|
|
255
|
+
typeof cfg?.max_retries === "number" ? cfg.max_retries : 3;
|
|
256
|
+
let attempt = 0;
|
|
257
|
+
|
|
258
|
+
for (;;) {
|
|
259
|
+
if (cfg?.log_requests) console.log(`Meta ${method} ${fullUrl}`);
|
|
260
|
+
const response = await fetch(fullUrl, { method, headers });
|
|
261
|
+
const body = await response.text();
|
|
262
|
+
let json;
|
|
263
|
+
try {
|
|
264
|
+
json = JSON.parse(body);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
if (response.status >= 500 && attempt < maxRetries) {
|
|
267
|
+
attempt += 1;
|
|
268
|
+
await sleep(1000 * Math.pow(2, attempt));
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
console.error(
|
|
272
|
+
`Meta Marketing API non-JSON response (HTTP ${response.status})`,
|
|
273
|
+
body
|
|
274
|
+
);
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Meta Marketing API: not a JSON response (HTTP ${response.status})`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (json && json.error) {
|
|
280
|
+
if (attempt < maxRetries && isTransientError(json.error)) {
|
|
281
|
+
attempt += 1;
|
|
282
|
+
await sleep(1000 * Math.pow(2, attempt));
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
throw mkApiError(json.error, response.status);
|
|
286
|
+
}
|
|
287
|
+
if (cfg?.log_requests) {
|
|
288
|
+
const usage = response.headers.get("x-business-use-case-usage");
|
|
289
|
+
if (usage) console.log("Meta business use case usage", usage);
|
|
290
|
+
}
|
|
291
|
+
return json;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Follow the cursor pagination on an edge and return the concatenated rows.
|
|
297
|
+
* Stops after max_pages pages so a misconfigured view cannot walk a whole
|
|
298
|
+
* account's history.
|
|
299
|
+
*/
|
|
300
|
+
const getAllPages = async (path, query, cfg = {}, opts = {}) => {
|
|
301
|
+
const maxPages = opts.max_pages || cfg?.max_pages || 10;
|
|
302
|
+
let json = await graphFetch(path, { query }, cfg);
|
|
303
|
+
const rows = [...(json?.data || [])];
|
|
304
|
+
let pages = 1;
|
|
305
|
+
while (json?.paging?.next && pages < maxPages) {
|
|
306
|
+
json = await graphFetch(json.paging.next, {}, cfg);
|
|
307
|
+
rows.push(...(json?.data || []));
|
|
308
|
+
pages += 1;
|
|
309
|
+
}
|
|
310
|
+
return rows;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const withFields = (query, kind) => {
|
|
314
|
+
const q = { ...(query || {}) };
|
|
315
|
+
if (!q.fields && DEFAULT_FIELDS[kind]) q.fields = DEFAULT_FIELDS[kind];
|
|
316
|
+
return q;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const withInsightsFields = (query) => {
|
|
320
|
+
const q = { ...(query || {}) };
|
|
321
|
+
if (!q.fields) q.fields = insightsFields(q.level);
|
|
322
|
+
return q;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
//
|
|
326
|
+
// Accounts and businesses
|
|
327
|
+
//
|
|
328
|
+
|
|
329
|
+
const getMe = async (query, cfg) =>
|
|
330
|
+
await graphFetch("/me", { query: { fields: "id,name", ...(query || {}) } }, cfg);
|
|
331
|
+
|
|
332
|
+
const getAdAccounts = async (query, cfg) =>
|
|
333
|
+
await getAllPages("/me/adaccounts", withFields(query, "adaccount"), cfg);
|
|
334
|
+
|
|
335
|
+
const getAdAccount = async (accountId, query, cfg) =>
|
|
336
|
+
await graphFetch(
|
|
337
|
+
`/${actId(accountId)}`,
|
|
338
|
+
{ query: withFields(query, "adaccount") },
|
|
339
|
+
cfg
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const getBusinesses = async (query, cfg) =>
|
|
343
|
+
await getAllPages("/me/businesses", withFields(query, "business"), cfg);
|
|
344
|
+
|
|
345
|
+
const getBusinessAdAccounts = async (businessId, query, cfg) =>
|
|
346
|
+
await getAllPages(
|
|
347
|
+
`/${businessId}/owned_ad_accounts`,
|
|
348
|
+
withFields(query, "adaccount"),
|
|
349
|
+
cfg
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
//
|
|
353
|
+
// Campaigns, ad sets, ads and creatives
|
|
354
|
+
//
|
|
355
|
+
|
|
356
|
+
const getCampaigns = async (accountId, query, cfg) =>
|
|
357
|
+
await getAllPages(
|
|
358
|
+
`/${actId(accountId)}/campaigns`,
|
|
359
|
+
withFields(query, "campaign"),
|
|
360
|
+
cfg
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const getCampaign = async (campaignId, query, cfg) =>
|
|
364
|
+
await graphFetch(
|
|
365
|
+
`/${campaignId}`,
|
|
366
|
+
{ query: withFields(query, "campaign") },
|
|
367
|
+
cfg
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const getAdSets = async (accountId, query, cfg) =>
|
|
371
|
+
await getAllPages(
|
|
372
|
+
`/${actId(accountId)}/adsets`,
|
|
373
|
+
withFields(query, "adset"),
|
|
374
|
+
cfg
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
const getCampaignAdSets = async (campaignId, query, cfg) =>
|
|
378
|
+
await getAllPages(
|
|
379
|
+
`/${campaignId}/adsets`,
|
|
380
|
+
withFields(query, "adset"),
|
|
381
|
+
cfg
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const getAdSet = async (adSetId, query, cfg) =>
|
|
385
|
+
await graphFetch(`/${adSetId}`, { query: withFields(query, "adset") }, cfg);
|
|
386
|
+
|
|
387
|
+
const getAds = async (accountId, query, cfg) =>
|
|
388
|
+
await getAllPages(`/${actId(accountId)}/ads`, withFields(query, "ad"), cfg);
|
|
389
|
+
|
|
390
|
+
const getCampaignAds = async (campaignId, query, cfg) =>
|
|
391
|
+
await getAllPages(`/${campaignId}/ads`, withFields(query, "ad"), cfg);
|
|
392
|
+
|
|
393
|
+
const getAdSetAds = async (adSetId, query, cfg) =>
|
|
394
|
+
await getAllPages(`/${adSetId}/ads`, withFields(query, "ad"), cfg);
|
|
395
|
+
|
|
396
|
+
const getAd = async (adId, query, cfg) =>
|
|
397
|
+
await graphFetch(`/${adId}`, { query: withFields(query, "ad") }, cfg);
|
|
398
|
+
|
|
399
|
+
const getAdCreatives = async (accountId, query, cfg) =>
|
|
400
|
+
await getAllPages(
|
|
401
|
+
`/${actId(accountId)}/adcreatives`,
|
|
402
|
+
withFields(query, "adcreative"),
|
|
403
|
+
cfg
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
const getAdCreative = async (creativeId, query, cfg) =>
|
|
407
|
+
await graphFetch(
|
|
408
|
+
`/${creativeId}`,
|
|
409
|
+
{ query: withFields(query, "adcreative") },
|
|
410
|
+
cfg
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
/** Rendered HTML preview of an ad, as an iframe snippet */
|
|
414
|
+
const getAdPreview = async (adId, adFormat, cfg) => {
|
|
415
|
+
const json = await graphFetch(
|
|
416
|
+
`/${adId}/previews`,
|
|
417
|
+
{ query: { ad_format: adFormat || "DESKTOP_FEED_STANDARD" } },
|
|
418
|
+
cfg
|
|
419
|
+
);
|
|
420
|
+
return json?.data?.[0]?.body || "";
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
//
|
|
424
|
+
// Insights
|
|
425
|
+
//
|
|
426
|
+
|
|
427
|
+
const getInsights = async (objectId, query, cfg, opts) =>
|
|
428
|
+
await getAllPages(
|
|
429
|
+
`/${objectId}/insights`,
|
|
430
|
+
withInsightsFields(query),
|
|
431
|
+
cfg,
|
|
432
|
+
opts
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
/** Kick off an asynchronous insights job, returns { report_run_id } */
|
|
436
|
+
const startInsightsReport = async (objectId, query, cfg) =>
|
|
437
|
+
await graphFetch(
|
|
438
|
+
`/${objectId}/insights`,
|
|
439
|
+
{ method: "POST", query: withInsightsFields(query) },
|
|
440
|
+
cfg
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const getReportRun = async (reportRunId, cfg) =>
|
|
444
|
+
await graphFetch(
|
|
445
|
+
`/${reportRunId}`,
|
|
446
|
+
{
|
|
447
|
+
query: {
|
|
448
|
+
fields:
|
|
449
|
+
"id,async_status,async_percent_completion,date_start,date_stop",
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
cfg
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const getReportRunInsights = async (reportRunId, query, cfg, opts) =>
|
|
456
|
+
await getAllPages(`/${reportRunId}/insights`, query, cfg, opts);
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* The full asynchronous insights flow: submit the job, poll it to completion
|
|
460
|
+
* and read the results. Use this for large reports that time out when read
|
|
461
|
+
* synchronously.
|
|
462
|
+
*/
|
|
463
|
+
const getInsightsAsync = async (objectId, query, cfg, opts = {}) => {
|
|
464
|
+
const pollInterval = opts.poll_interval_ms || 5000;
|
|
465
|
+
const timeout = opts.timeout_ms || 10 * 60 * 1000;
|
|
466
|
+
const { report_run_id } = await startInsightsReport(objectId, query, cfg);
|
|
467
|
+
if (!report_run_id)
|
|
468
|
+
throw new Error("Meta Marketing API: no report_run_id returned");
|
|
469
|
+
const startedAt = Date.now();
|
|
470
|
+
for (;;) {
|
|
471
|
+
const run = await getReportRun(report_run_id, cfg);
|
|
472
|
+
if (run?.async_status === "Job Completed") break;
|
|
473
|
+
if (["Job Failed", "Job Skipped"].includes(run?.async_status))
|
|
474
|
+
throw new Error(
|
|
475
|
+
`Meta Marketing API: insights job ${run.async_status} (${report_run_id})`
|
|
476
|
+
);
|
|
477
|
+
if (Date.now() - startedAt > timeout)
|
|
478
|
+
throw new Error(
|
|
479
|
+
`Meta Marketing API: insights job timed out (${report_run_id})`
|
|
480
|
+
);
|
|
481
|
+
await sleep(pollInterval);
|
|
482
|
+
}
|
|
483
|
+
return await getReportRunInsights(report_run_id, {}, cfg, opts);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
//
|
|
487
|
+
// Tokens
|
|
488
|
+
//
|
|
489
|
+
|
|
490
|
+
/** Exchange a short lived (or expiring) user token for a long lived one */
|
|
491
|
+
const exchangeLongLivedToken = async (app_id, app_secret, access_token, cfg) =>
|
|
492
|
+
await graphFetch(
|
|
493
|
+
"/oauth/access_token",
|
|
494
|
+
{
|
|
495
|
+
noAuth: true,
|
|
496
|
+
query: {
|
|
497
|
+
grant_type: "fb_exchange_token",
|
|
498
|
+
client_id: app_id,
|
|
499
|
+
client_secret: app_secret,
|
|
500
|
+
fb_exchange_token: access_token,
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
cfg
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
/** Inspect a token: which app it belongs to, when it expires, its scopes */
|
|
507
|
+
const debugToken = async (token, app_id, app_secret, cfg) => {
|
|
508
|
+
const json = await graphFetch(
|
|
509
|
+
"/debug_token",
|
|
510
|
+
{
|
|
511
|
+
noAuth: true,
|
|
512
|
+
query: {
|
|
513
|
+
input_token: token,
|
|
514
|
+
access_token: `${app_id}|${app_secret}`,
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
cfg
|
|
518
|
+
);
|
|
519
|
+
return json?.data || json;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
module.exports = {
|
|
523
|
+
GRAPH_HOST,
|
|
524
|
+
DEFAULT_API_VERSION,
|
|
525
|
+
DEFAULT_FIELDS,
|
|
526
|
+
BASE_INSIGHTS_FIELDS,
|
|
527
|
+
INSIGHTS_LEVEL_FIELDS,
|
|
528
|
+
NUMERIC_INSIGHTS_FIELDS,
|
|
529
|
+
insightsFields,
|
|
530
|
+
queryValue,
|
|
531
|
+
toQueryString,
|
|
532
|
+
actId,
|
|
533
|
+
appSecretProof,
|
|
534
|
+
isTransientError,
|
|
535
|
+
graphFetch,
|
|
536
|
+
getAllPages,
|
|
537
|
+
getMe,
|
|
538
|
+
getAdAccounts,
|
|
539
|
+
getAdAccount,
|
|
540
|
+
getBusinesses,
|
|
541
|
+
getBusinessAdAccounts,
|
|
542
|
+
getCampaigns,
|
|
543
|
+
getCampaign,
|
|
544
|
+
getAdSets,
|
|
545
|
+
getCampaignAdSets,
|
|
546
|
+
getAdSet,
|
|
547
|
+
getAds,
|
|
548
|
+
getCampaignAds,
|
|
549
|
+
getAdSetAds,
|
|
550
|
+
getAd,
|
|
551
|
+
getAdCreatives,
|
|
552
|
+
getAdCreative,
|
|
553
|
+
getAdPreview,
|
|
554
|
+
getInsights,
|
|
555
|
+
startInsightsReport,
|
|
556
|
+
getReportRun,
|
|
557
|
+
getReportRunInsights,
|
|
558
|
+
getInsightsAsync,
|
|
559
|
+
exchangeLongLivedToken,
|
|
560
|
+
debugToken,
|
|
561
|
+
};
|