@mhubcloud/tms_core 3.4.0-beta.3 → 3.4.0-beta.4

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/dist/index.js CHANGED
@@ -47,98 +47,6 @@ __export(broker_exports, {
47
47
  });
48
48
  var import_cosmos = require("@azure/cosmos");
49
49
  var import_service_bus = require("@azure/service-bus");
50
- var serviceBusConnectionString = process.env.ServiceBusConnectionString;
51
- var cosmosConnectionString = process.env.CosmosDbConnectionString;
52
- var cosmosDatabaseName = process.env.CosmosDbDatabaseName;
53
- var cosmosContainerName = process.env.CosmosEventsContainerName;
54
- var serviceBusClient;
55
- var cosmosClient;
56
- var senders = {};
57
- function resolveEvent(eventCode, publisherConfig) {
58
- if (!publisherConfig) {
59
- const envConfig = process.env.Broker_PublisherConfig;
60
- publisherConfig = envConfig ? JSON.parse(envConfig) : {};
61
- }
62
- const publishers = [];
63
- for (const publisher2 of Object.keys(publisherConfig)) {
64
- const publisherEvents = publisherConfig[publisher2];
65
- if (publisherEvents[0] === "*") {
66
- let matchesExcluded = false;
67
- if (Array.isArray(publisherEvents[1]) && publisherEvents[1].length > 0) {
68
- for (const event of publisherEvents[1]) {
69
- if (matchesExcluded) {
70
- break;
71
- }
72
- if (typeof event === "string") {
73
- const regex = new RegExp(event);
74
- matchesExcluded = regex.test(eventCode);
75
- }
76
- }
77
- }
78
- if (!matchesExcluded) {
79
- publishers.push(publisher2);
80
- }
81
- } else {
82
- let match = false;
83
- for (const event of publisherEvents) {
84
- if (typeof event === "string") {
85
- const regex = new RegExp(event);
86
- match = regex.test(eventCode);
87
- }
88
- if (match) {
89
- publishers.push(publisher2);
90
- }
91
- }
92
- }
93
- }
94
- if (publishers.length === 0) {
95
- return null;
96
- }
97
- return publishers;
98
- }
99
- async function dispatchEvent(message, queues) {
100
- if (!serviceBusClient) {
101
- if (!serviceBusConnectionString) {
102
- throw new Error("Service Bus connection string is missing.");
103
- }
104
- serviceBusClient = new import_service_bus.ServiceBusClient(serviceBusConnectionString);
105
- }
106
- await Promise.all(
107
- queues.map(async (queue) => {
108
- try {
109
- let sender;
110
- if (senders[queue]) {
111
- sender = senders[queue];
112
- } else {
113
- senders[queue] = serviceBusClient.createSender(queue);
114
- sender = senders[queue];
115
- }
116
- await sender.sendMessages({ body: message });
117
- } catch (e) {
118
- console.log(`Sending to queue ${queue} failed`);
119
- }
120
- })
121
- );
122
- }
123
- async function logToCosmosDb(message, logger) {
124
- if (!cosmosClient) {
125
- if (!cosmosConnectionString) {
126
- throw new Error("Cosmos DB connection string is missing.");
127
- }
128
- cosmosClient = new import_cosmos.CosmosClient(cosmosConnectionString);
129
- }
130
- if (!cosmosDatabaseName) {
131
- throw new Error("Cosmos DB database name is missing.");
132
- }
133
- if (!cosmosContainerName) {
134
- throw new Error("Cosmos DB events container name is missing.");
135
- }
136
- try {
137
- await cosmosClient.database(cosmosDatabaseName).container(cosmosContainerName).items.create(message);
138
- } catch (e) {
139
- utils_exports.logException(logger, e);
140
- }
141
- }
142
50
 
143
51
  // src/utils/utils.ts
144
52
  var utils_exports = {};
@@ -147,19 +55,19 @@ __export(utils_exports, {
147
55
  encodeEmail: () => encodeEmail,
148
56
  extractAdCode: () => extractAdCode,
149
57
  flatten: () => flatten,
58
+ formatError: () => formatError,
150
59
  handlePlatformRequest: () => handlePlatformRequest,
151
60
  handleResponse: () => handleResponse,
152
61
  logEvent: () => logEvent,
153
62
  logException: () => logException,
154
63
  logMessage: () => logMessage,
155
- parseErrorMessage: () => parseErrorMessage,
156
64
  prepareTestRequestHeaders: () => prepareTestRequestHeaders,
157
65
  removeUndefined: () => removeUndefined
158
66
  });
159
67
  var crypto = __toESM(require("crypto"));
160
68
  var appInsights = __toESM(require("applicationinsights"));
161
69
  var aiClient = null;
162
- if (process.env.APPLICATIONINSIGHTS_CONNECTION_STRING || process.env.APPINSIGHTS_INSTRUMENTATIONKEY) {
70
+ if (process.env.APPLICATIONINSIGHTS_CONNECTION_STRING) {
163
71
  appInsights.setup().setAutoCollectDependencies(false).setAutoCollectConsole(true, false).setSendLiveMetrics(false).setAutoCollectRequests(false).setAutoCollectPerformance(false).setUseDiskRetryCaching(false).start();
164
72
  aiClient = appInsights.defaultClient;
165
73
  }
@@ -192,15 +100,17 @@ function logMessage(level, message, properties) {
192
100
  properties
193
101
  });
194
102
  }
195
- function logEvent(publisher2, event, region) {
103
+ function logEvent(publisher2, event, region, status, payload) {
196
104
  const properties = {
197
105
  publisher: publisher2,
198
106
  event,
199
- region
107
+ region,
108
+ status,
109
+ payload
200
110
  };
201
111
  logMessage(1, "Event dispatched to platform", properties);
202
112
  }
203
- function logException(log2, exception) {
113
+ function logException(exception) {
204
114
  aiClient?.trackException({ exception });
205
115
  }
206
116
  function removeUndefined(originalValue) {
@@ -314,7 +224,10 @@ async function handlePlatformRequest({
314
224
  // eslint-disable-next-line @typescript-eslint/naming-convention
315
225
  headers: { ...requestOptions.headers }
316
226
  };
317
- const rawPlatformResponse = await fetch(fullUrl, fetchRequestOptions);
227
+ const rawPlatformResponse = await fetch(
228
+ fullUrl,
229
+ fetchRequestOptions
230
+ );
318
231
  if (process.env.PUBLISHER_TESTURL) {
319
232
  await handleTestRequest({
320
233
  fetchRequestOptions,
@@ -328,10 +241,22 @@ async function handlePlatformRequest({
328
241
  rawPlatformResponse,
329
242
  event.event
330
243
  );
331
- logEvent(source, event.event, event.region);
244
+ logEvent(
245
+ source,
246
+ event.event,
247
+ event.region,
248
+ rawPlatformResponse.status,
249
+ requestOptions.body ? JSON.stringify(requestOptions.body) : void 0
250
+ );
332
251
  return processedPlatformResponse;
333
252
  } else {
334
- logEvent(source, event.event, event.region);
253
+ logEvent(
254
+ source,
255
+ event.event,
256
+ event.region,
257
+ rawPlatformResponse.status,
258
+ requestOptions.body ? JSON.stringify(requestOptions.body) : void 0
259
+ );
335
260
  }
336
261
  }
337
262
  async function handleTestRequest(params) {
@@ -354,18 +279,115 @@ async function handleTestRequest(params) {
354
279
  } catch {
355
280
  }
356
281
  }
357
- function parseErrorMessage(error) {
358
- let errorMessage;
282
+ function formatError(error) {
283
+ let formattedError;
359
284
  if (error instanceof Error) {
285
+ formattedError = error;
360
286
  if (error.cause) {
361
- errorMessage = `${error.message} Cause: ${error.cause.toString()}`;
287
+ formattedError.message = `${error.message} Cause: ${error.cause.toString()}`;
362
288
  } else {
363
- errorMessage = error.message;
289
+ formattedError.message = error.message;
364
290
  }
365
291
  } else {
366
- errorMessage = error;
292
+ formattedError = new Error(error);
293
+ }
294
+ return formattedError;
295
+ }
296
+
297
+ // src/broker.ts
298
+ var serviceBusConnectionString = process.env.ServiceBusConnectionString;
299
+ var cosmosConnectionString = process.env.CosmosDbConnectionString;
300
+ var cosmosDatabaseName = process.env.CosmosDbDatabaseName;
301
+ var cosmosContainerName = process.env.CosmosDbEventsContainerName;
302
+ var serviceBusClient;
303
+ var cosmosClient;
304
+ var senders = {};
305
+ function resolveEvent(eventCode, publisherConfig) {
306
+ if (!publisherConfig) {
307
+ const envConfig = process.env.Broker_PublisherConfig;
308
+ publisherConfig = envConfig ? JSON.parse(envConfig) : {};
309
+ }
310
+ const publishers = [];
311
+ for (const publisher2 of Object.keys(publisherConfig)) {
312
+ const publisherEvents = publisherConfig[publisher2];
313
+ if (publisherEvents) {
314
+ if (publisherEvents[0] === "*") {
315
+ let matchesExcluded = false;
316
+ if (Array.isArray(publisherEvents[1]) && publisherEvents[1].length > 0) {
317
+ for (const event of publisherEvents[1]) {
318
+ if (matchesExcluded) {
319
+ break;
320
+ }
321
+ if (typeof event === "string") {
322
+ const regex = new RegExp(event);
323
+ matchesExcluded = regex.test(eventCode);
324
+ }
325
+ }
326
+ }
327
+ if (!matchesExcluded) {
328
+ publishers.push(publisher2);
329
+ }
330
+ } else {
331
+ let match = false;
332
+ for (const event of publisherEvents) {
333
+ if (typeof event === "string") {
334
+ const regex = new RegExp(event);
335
+ match = regex.test(eventCode);
336
+ }
337
+ if (match) {
338
+ publishers.push(publisher2);
339
+ }
340
+ }
341
+ }
342
+ }
343
+ }
344
+ if (publishers.length === 0) {
345
+ return null;
346
+ }
347
+ return publishers;
348
+ }
349
+ async function dispatchEvent(message, queues) {
350
+ if (!serviceBusClient) {
351
+ if (!serviceBusConnectionString) {
352
+ throw new Error("Service Bus connection string is missing.");
353
+ }
354
+ serviceBusClient = new import_service_bus.ServiceBusClient(serviceBusConnectionString);
355
+ }
356
+ await Promise.all(
357
+ queues.map(async (queue) => {
358
+ try {
359
+ let sender;
360
+ if (senders[queue]) {
361
+ sender = senders[queue];
362
+ } else {
363
+ senders[queue] = serviceBusClient.createSender(queue);
364
+ sender = senders[queue];
365
+ }
366
+ await sender.sendMessages({ body: message });
367
+ } catch (e) {
368
+ console.log(`Sending to queue ${queue} failed`);
369
+ }
370
+ })
371
+ );
372
+ }
373
+ async function logToCosmosDb(message) {
374
+ if (!cosmosClient) {
375
+ if (!cosmosConnectionString) {
376
+ throw new Error("Cosmos DB connection string is missing.");
377
+ }
378
+ cosmosClient = new import_cosmos.CosmosClient(cosmosConnectionString);
379
+ }
380
+ if (!cosmosDatabaseName) {
381
+ throw new Error("Cosmos DB database name is missing.");
382
+ }
383
+ if (!cosmosContainerName) {
384
+ throw new Error("Cosmos DB events container name is missing.");
385
+ }
386
+ try {
387
+ await cosmosClient.database(cosmosDatabaseName).container(cosmosContainerName).items.create(message);
388
+ } catch (e) {
389
+ logException(e);
367
390
  }
368
- return errorMessage;
369
391
  }
370
392
 
371
393
  // src/defaultPublisher.ts
@@ -386,15 +408,21 @@ async function defaultPublisher(azureFunctionContext, serviceBusMessage, source,
386
408
  for (const result of results) {
387
409
  if (result.status === "rejected") {
388
410
  if (result.reason instanceof Error) {
389
- const errorMessage = parseErrorMessage(result.reason);
390
- logException(azureFunctionContext.log, new Error(errorMessage));
391
- throw new Error(errorMessage);
411
+ const formattedError = formatError(result.reason);
412
+ logException(
413
+ azureFunctionContext.log,
414
+ new Error(formattedError.message)
415
+ );
416
+ throw formattedError;
392
417
  } else {
393
- const errorMessage = parseErrorMessage(
418
+ const formattedError = formatError(
394
419
  typeof result.reason === "object" ? JSON.stringify(result.reason) : result.reason
395
420
  );
396
- logException(azureFunctionContext.log, new Error(errorMessage));
397
- throw new Error(errorMessage);
421
+ logException(
422
+ azureFunctionContext.log,
423
+ new Error(formattedError.message)
424
+ );
425
+ throw formattedError;
398
426
  }
399
427
  }
400
428
  }
@@ -580,42 +608,9 @@ var facebook = {
580
608
  }
581
609
  };
582
610
 
583
- // src/utils/tokens/googleAdsToken.ts
584
- var tokenCache = null;
585
- var tokenPromise = null;
586
- async function fetchGoogleAdsToken(config) {
587
- if (tokenCache && tokenCache.createdAt + 3500 * 1e3 > Date.now()) {
588
- return tokenCache;
589
- }
590
- if (!tokenPromise) {
591
- tokenPromise = (async () => {
592
- const tokenResponse = await fetch(`https://oauth2.googleapis.com/token`, {
593
- method: "POST",
594
- headers: {
595
- // eslint-disable-next-line @typescript-eslint/naming-convention
596
- "Content-Type": "application/x-www-form-urlencoded"
597
- },
598
- body: new URLSearchParams({
599
- // eslint-disable-next-line @typescript-eslint/naming-convention
600
- client_id: config.client.id,
601
- // eslint-disable-next-line @typescript-eslint/naming-convention
602
- client_secret: config.client.secret,
603
- refreshToken: config.refreshToken,
604
- // eslint-disable-next-line @typescript-eslint/naming-convention
605
- grant_type: "refresh_token"
606
- })
607
- });
608
- const parsedTokenResponse = await tokenResponse.json();
609
- const createdAt = Date.now();
610
- tokenCache = { token: parsedTokenResponse.access_token, createdAt };
611
- tokenPromise = null;
612
- return tokenCache;
613
- })();
614
- }
615
- return tokenPromise;
616
- }
617
-
618
611
  // src/publishers/googleAds.ts
612
+ var tokenCache;
613
+ var tokenPromise;
619
614
  var googleAds = {
620
615
  source: "GoogleAds",
621
616
  /**
@@ -630,7 +625,7 @@ var googleAds = {
630
625
  logMessage(1, `Excluded ${event.event} due to lack of identifiers`);
631
626
  return;
632
627
  }
633
- const tokenData = await fetchGoogleAdsToken(config);
628
+ const tokenData = await this.getToken(config);
634
629
  data.conversionAction = `customers/${config.customerId}/conversionActions/${config.conversionId}`;
635
630
  const body = {
636
631
  conversions: [data],
@@ -643,6 +638,7 @@ var googleAds = {
643
638
  headers: {
644
639
  // eslint-disable-next-line @typescript-eslint/naming-convention
645
640
  Authorization: `Bearer ${tokenData.token}`,
641
+ // eslint-disable-next-line @typescript-eslint/naming-convention
646
642
  "developer-token": config.client.developerToken
647
643
  },
648
644
  body: JSON.stringify(body)
@@ -651,23 +647,68 @@ var googleAds = {
651
647
  const response = await fetch(baseUrl, requestOptions);
652
648
  const responseJson = await response.json();
653
649
  const knownErrors = [
654
- "The imported event has a conversion_date_time that precedes the click. Make sure your conversion_date_time is correct and try again., at conversions[0].conversion_date_time",
655
- "The click ID or call is associated with an Ads account you don't have access to. Make sure you import conversions for accounts managed by your manager account., at conversions[0].gclid",
656
- "The imported event can't be recorded because its click occurred before this conversion's click-through window. Make sure you import the most recent data., at conversions[0].gclid",
657
- "The imported event includes an order ID that was previously recorded, so the event was not processed., at conversions[0].order_id",
658
- "The imported event could not be attributed to a click. This may be because the event did not come from a Google Ads campaign., at conversions[0].gclid",
659
- "The imported event can't be recorded because its click occurred before this conversion's click-through window. Make sure you import the most recent data., at conversions[0].conversion_action"
650
+ "The imported event has a conversion_date_time that precedes the click. Make sure your conversion_date_time is correct and try again.",
651
+ "The click ID or call is associated with an Ads account you don't have access to. Make sure you import conversions for accounts managed by your manager account.",
652
+ "The imported event can't be recorded because its click occurred before this conversion's click-through window. Make sure you import the most recent data.",
653
+ "The imported event includes an order ID that was previously recorded, so the event was not processed.",
654
+ "The imported event could not be attributed to a click. This may be because the event did not come from a Google Ads campaign.",
655
+ "The imported event can't be recorded because its click occurred before this conversion's click-through window. Make sure you import the most recent data."
660
656
  ];
661
657
  if (responseJson.partialFailureError) {
662
658
  logMessage(1, responseJson.partialFailureError.message);
663
- if (!knownErrors.includes(responseJson.partialFailureError.message)) {
659
+ const trimmedMessage = responseJson.partialFailureError.message.split("., at")[0] ?? "";
660
+ if (!knownErrors.includes(trimmedMessage)) {
664
661
  throw new Error(responseJson.partialFailureError.message);
665
662
  }
666
663
  } else if (responseJson.error) {
667
664
  throw new Error(responseJson.error.message);
668
665
  } else {
669
- logEvent(this.source, event.event, event.region);
666
+ logEvent(
667
+ this.source,
668
+ event.event,
669
+ event.region,
670
+ response.status,
671
+ requestOptions.body
672
+ );
673
+ }
674
+ },
675
+ async getToken(config) {
676
+ if (tokenCache && tokenCache.createdAt + 3500 * 1e3 > Date.now()) {
677
+ return tokenCache;
678
+ }
679
+ if (!tokenPromise) {
680
+ tokenPromise = (async () => {
681
+ const tokenResponse = await fetch(
682
+ `https://oauth2.googleapis.com/token`,
683
+ {
684
+ method: "POST",
685
+ headers: {
686
+ // eslint-disable-next-line @typescript-eslint/naming-convention
687
+ "Content-Type": "application/x-www-form-urlencoded"
688
+ },
689
+ body: new URLSearchParams({
690
+ // eslint-disable-next-line @typescript-eslint/naming-convention
691
+ client_id: config.client.id,
692
+ // eslint-disable-next-line @typescript-eslint/naming-convention
693
+ client_secret: config.client.secret,
694
+ // eslint-disable-next-line @typescript-eslint/naming-convention
695
+ refreshToken: config.refreshToken,
696
+ // eslint-disable-next-line @typescript-eslint/naming-convention
697
+ grant_type: "refresh_token"
698
+ })
699
+ }
700
+ );
701
+ if (!tokenResponse.ok) {
702
+ throw new Error("Access token fetch failed");
703
+ }
704
+ const parsedTokenResponse = await tokenResponse.json();
705
+ const createdAt = Date.now();
706
+ tokenCache = { token: parsedTokenResponse.access_token, createdAt };
707
+ tokenPromise = null;
708
+ return tokenCache;
709
+ })();
670
710
  }
711
+ return tokenPromise;
671
712
  }
672
713
  };
673
714
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhubcloud/tms_core",
3
- "version": "3.4.0-beta.3",
3
+ "version": "3.4.0-beta.4",
4
4
  "description": "Core function for mHub Cloud TMS publishers",
5
5
  "homepage": "https://mhubcloud.com/",
6
6
  "license": "Apache 2.0",
package/src/broker.ts CHANGED
@@ -5,140 +5,138 @@ import * as utils from './utils/utils';
5
5
  const serviceBusConnectionString = process.env.ServiceBusConnectionString;
6
6
  const cosmosConnectionString = process.env.CosmosDbConnectionString;
7
7
  const cosmosDatabaseName = process.env.CosmosDbDatabaseName;
8
- const cosmosContainerName = process.env.CosmosEventsContainerName;
8
+ const cosmosContainerName = process.env.CosmosDbEventsContainerName;
9
9
  let serviceBusClient: ServiceBusClient | undefined;
10
10
  let cosmosClient: CosmosClient | undefined;
11
11
  const senders: Record<string, ServiceBusSender> = {};
12
12
 
13
- type PublisherConfig = Record<string, string[] | string[][]>;
13
+ export type PublisherConfig = Record<string, string[] | string[][]>;
14
14
 
15
- export const broker = {
16
- /**
17
- * Evaluates an event against publisher configurations
18
- * @param eventCode event.code parameter of the event
19
- * @param publisherConfig object containing publisher event consumption configurations
20
- * @returns list of publishers for which an event is relevant.
21
- */
22
- resolveEvent: function (
23
- eventCode: string,
24
- publisherConfig?: PublisherConfig,
25
- ) {
26
- if (!publisherConfig) {
27
- const envConfig = process.env.Broker_PublisherConfig;
28
- publisherConfig = envConfig ? JSON.parse(envConfig) : {};
29
- }
30
-
31
- const publishers = [];
15
+ /**
16
+ * Evaluates an event against publisher configurations
17
+ * @param eventCode event.code parameter of the event
18
+ * @param publisherConfig object containing publisher event consumption configurations
19
+ * @returns list of publishers for which an event is relevant.
20
+ */
21
+ export function resolveEvent(
22
+ eventCode: string,
23
+ publisherConfig?: PublisherConfig,
24
+ ) {
25
+ if (!publisherConfig) {
26
+ const envConfig = process.env.Broker_PublisherConfig;
27
+ publisherConfig = envConfig ? JSON.parse(envConfig) : {};
28
+ }
32
29
 
33
- for (const publisher of Object.keys(publisherConfig)) {
34
- const publisherEvents = publisherConfig[publisher];
35
- if (publisherEvents) {
36
- if (publisherEvents[0] === '*') {
37
- let matchesExcluded = false;
38
- // Checking whether event matches any of the excluded events
39
- if (
40
- Array.isArray(publisherEvents[1]) &&
41
- publisherEvents[1].length > 0
42
- ) {
43
- for (const event of publisherEvents[1]) {
44
- if (matchesExcluded) {
45
- break;
46
- }
30
+ const publishers = [];
47
31
 
48
- if (typeof event === 'string') {
49
- const regex = new RegExp(event);
50
- matchesExcluded = regex.test(eventCode);
51
- }
32
+ for (const publisher of Object.keys(publisherConfig)) {
33
+ const publisherEvents = publisherConfig[publisher];
34
+ if (publisherEvents) {
35
+ if (publisherEvents[0] === '*') {
36
+ let matchesExcluded = false;
37
+ // Checking whether event matches any of the excluded events
38
+ if (
39
+ Array.isArray(publisherEvents[1]) &&
40
+ publisherEvents[1].length > 0
41
+ ) {
42
+ for (const event of publisherEvents[1]) {
43
+ if (matchesExcluded) {
44
+ break;
52
45
  }
53
- }
54
- if (!matchesExcluded) {
55
- publishers.push(publisher);
56
- }
57
- } else {
58
- let match = false;
59
46
 
60
- for (const event of publisherEvents) {
61
47
  if (typeof event === 'string') {
62
48
  const regex = new RegExp(event);
63
- match = regex.test(eventCode);
49
+ matchesExcluded = regex.test(eventCode);
64
50
  }
51
+ }
52
+ }
53
+ if (!matchesExcluded) {
54
+ publishers.push(publisher);
55
+ }
56
+ } else {
57
+ let match = false;
65
58
 
66
- if (match) {
67
- publishers.push(publisher);
68
- }
59
+ for (const event of publisherEvents) {
60
+ if (typeof event === 'string') {
61
+ const regex = new RegExp(event);
62
+ match = regex.test(eventCode);
63
+ }
64
+
65
+ if (match) {
66
+ publishers.push(publisher);
69
67
  }
70
68
  }
71
69
  }
72
70
  }
71
+ }
73
72
 
74
- if (publishers.length === 0) {
75
- return null;
76
- }
73
+ if (publishers.length === 0) {
74
+ return null;
75
+ }
77
76
 
78
- return publishers;
79
- },
77
+ return publishers;
78
+ }
80
79
 
81
- /**
82
- * Sends an event to Service Bus
83
- * @param message event to be sent
84
- * @param queues list of Service Bus queues to send the event to
85
- */
86
- dispatchEvent: async function (
87
- message: Record<string, string>,
88
- queues: string[],
89
- ) {
90
- if (!serviceBusClient) {
91
- if (!serviceBusConnectionString) {
92
- throw new Error('Service Bus connection string is missing.');
93
- }
94
- serviceBusClient = new ServiceBusClient(serviceBusConnectionString);
80
+ /**
81
+ * Sends an event to Service Bus
82
+ * @param message event to be sent
83
+ * @param queues list of Service Bus queues to send the event to
84
+ */
85
+ export async function dispatchEvent(
86
+ message: Record<string, string>,
87
+ queues: string[],
88
+ ) {
89
+ if (!serviceBusClient) {
90
+ if (!serviceBusConnectionString) {
91
+ throw new Error('Service Bus connection string is missing.');
95
92
  }
93
+ serviceBusClient = new ServiceBusClient(serviceBusConnectionString);
94
+ }
96
95
 
97
- await Promise.all(
98
- queues.map(async (queue) => {
99
- try {
100
- let sender;
101
- if (senders[queue]) {
102
- sender = senders[queue];
103
- } else {
104
- senders[queue] = serviceBusClient!.createSender(queue);
105
- sender = senders[queue];
106
- }
107
- await sender.sendMessages({ body: message });
108
- } catch (e) {
109
- console.log(`Sending to queue ${queue} failed`);
96
+ await Promise.all(
97
+ queues.map(async (queue) => {
98
+ try {
99
+ let sender;
100
+ if (senders[queue]) {
101
+ sender = senders[queue];
102
+ } else {
103
+ senders[queue] = serviceBusClient!.createSender(queue);
104
+ sender = senders[queue];
110
105
  }
111
- }),
112
- );
113
- },
114
-
115
- /**
116
- * Logs an event to Cosmos DB
117
- * @param message event to be logged
118
- */
119
- logToCosmosDb: async function (message: ServiceBusMessage) {
120
- if (!cosmosClient) {
121
- if (!cosmosConnectionString) {
122
- throw new Error('Cosmos DB connection string is missing.');
106
+ await sender.sendMessages({ body: message });
107
+ } catch (e) {
108
+ console.log(`Sending to queue ${queue} failed`);
123
109
  }
124
- cosmosClient = new CosmosClient(cosmosConnectionString);
125
- }
110
+ }),
111
+ );
112
+ }
126
113
 
127
- if (!cosmosDatabaseName) {
128
- throw new Error('Cosmos DB database name is missing.');
114
+ /**
115
+ * Logs an event to Cosmos DB
116
+ * @param message event to be logged
117
+ */
118
+ export async function logToCosmosDb(message: ServiceBusMessage) {
119
+ if (!cosmosClient) {
120
+ if (!cosmosConnectionString) {
121
+ throw new Error('Cosmos DB connection string is missing.');
129
122
  }
123
+ cosmosClient = new CosmosClient(cosmosConnectionString);
124
+ }
130
125
 
131
- if (!cosmosContainerName) {
132
- throw new Error('Cosmos DB events container name is missing.');
133
- }
126
+ if (!cosmosDatabaseName) {
127
+ throw new Error('Cosmos DB database name is missing.');
128
+ }
134
129
 
135
- try {
136
- await cosmosClient
137
- .database(cosmosDatabaseName)
138
- .container(cosmosContainerName)
139
- .items.create(message);
140
- } catch (e) {
141
- utils.logException(e as Error);
142
- }
143
- },
144
- };
130
+ if (!cosmosContainerName) {
131
+ throw new Error('Cosmos DB events container name is missing.');
132
+ }
133
+
134
+ try {
135
+ await cosmosClient
136
+ .database(cosmosDatabaseName)
137
+ .container(cosmosContainerName)
138
+ .items.create(message);
139
+ } catch (e) {
140
+ utils.logException(e as Error);
141
+ }
142
+ }
@@ -40,17 +40,23 @@ export async function defaultPublisher(
40
40
  for (const result of results) {
41
41
  if (result.status === 'rejected') {
42
42
  if (result.reason instanceof Error) {
43
- const errorMessage = utils.parseErrorMessage(result.reason);
44
- utils.logException(azureFunctionContext.log, new Error(errorMessage));
45
- throw new Error(errorMessage);
43
+ const formattedError = utils.formatError(result.reason);
44
+ utils.logException(
45
+ azureFunctionContext.log,
46
+ new Error(formattedError.message),
47
+ );
48
+ throw formattedError;
46
49
  } else {
47
- const errorMessage = utils.parseErrorMessage(
50
+ const formattedError = utils.formatError(
48
51
  typeof result.reason === 'object'
49
52
  ? JSON.stringify(result.reason)
50
53
  : (result.reason as Error['message']),
51
54
  );
52
- utils.logException(azureFunctionContext.log, new Error(errorMessage));
53
- throw new Error(errorMessage);
55
+ utils.logException(
56
+ azureFunctionContext.log,
57
+ new Error(formattedError.message),
58
+ );
59
+ throw formattedError;
54
60
  }
55
61
  }
56
62
  }
@@ -1,24 +1,65 @@
1
+ import { Logger } from '@azure/functions';
1
2
  import * as utils from './../utils/utils';
2
- import {
3
- PlatformPublisher,
4
- PlatformRequestOptions,
5
- } from '../types/PlatformPublisher';
6
- import {
7
- fetchGoogleAdsToken,
8
- GoogleAdsConfig,
9
- } from '../utils/tokens/googleAdsToken';
3
+ import { EventToLog } from '../types/EventToLog';
4
+ import { TypeSafeObject } from '../types/UtilTypes';
5
+
6
+ interface GoogleAdsPlatformPublisher {
7
+ source: string;
8
+ publish: (
9
+ log: Logger,
10
+ config: GoogleAdsConfig,
11
+ data: TypeSafeObject,
12
+ event: EventToLog,
13
+ ) => Promise<void>;
14
+ getToken: (config: GoogleAdsConfig) => Promise<TokenCache>;
15
+ }
10
16
 
11
17
  interface GoogleAdsData {
12
18
  gclid?: string;
13
19
  wbraid?: string;
14
20
  gbraid?: string;
15
21
  // eslint-disable-next-line @typescript-eslint/naming-convention
16
- user_identifiers: Record<string, unknown>[];
22
+ user_identifiers?: Record<string, unknown>[];
17
23
  conversionAction?: string;
18
24
  [x: string]: unknown;
19
25
  }
20
26
 
21
- export const googleAds: PlatformPublisher = {
27
+ interface GoogleAdsConfig extends TypeSafeObject {
28
+ client: {
29
+ id: string;
30
+ secret: string;
31
+ developerToken: string;
32
+ };
33
+ refreshToken: string;
34
+ customerId: string;
35
+ conversionId: string;
36
+ }
37
+
38
+ interface GoogleAdsResponse {
39
+ partialFailureError?: {
40
+ message: string;
41
+ };
42
+ error?: {
43
+ message: string;
44
+ };
45
+ }
46
+
47
+ interface TokenCache {
48
+ token: string;
49
+ createdAt: number;
50
+ }
51
+
52
+ interface TokenResponse {
53
+ // eslint-disable-next-line @typescript-eslint/naming-convention
54
+ access_token: string;
55
+ }
56
+
57
+ type TokenPromise = Promise<TokenCache> | null;
58
+
59
+ let tokenCache: TokenCache | null;
60
+ let tokenPromise: TokenPromise;
61
+
62
+ export const googleAds: GoogleAdsPlatformPublisher = {
22
63
  source: 'GoogleAds',
23
64
  /**
24
65
  *Publishes data to Google Ads
@@ -38,7 +79,7 @@ export const googleAds: PlatformPublisher = {
38
79
  return;
39
80
  }
40
81
 
41
- const tokenData = await fetchGoogleAdsToken(config);
82
+ const tokenData = await this.getToken(config);
42
83
 
43
84
  data.conversionAction = `customers/${config.customerId}/conversionActions/${config.conversionId}`;
44
85
 
@@ -54,6 +95,7 @@ export const googleAds: PlatformPublisher = {
54
95
  headers: {
55
96
  // eslint-disable-next-line @typescript-eslint/naming-convention
56
97
  Authorization: `Bearer ${tokenData.token}`,
98
+ // eslint-disable-next-line @typescript-eslint/naming-convention
57
99
  'developer-token': config.client.developerToken,
58
100
  },
59
101
  body: JSON.stringify(body),
@@ -65,32 +107,75 @@ export const googleAds: PlatformPublisher = {
65
107
  const responseJson: GoogleAdsResponse = await response.json();
66
108
 
67
109
  const knownErrors = [
68
- 'The imported event has a conversion_date_time that precedes the click. Make sure your conversion_date_time is correct and try again., at conversions[0].conversion_date_time',
69
- "The click ID or call is associated with an Ads account you don't have access to. Make sure you import conversions for accounts managed by your manager account., at conversions[0].gclid",
70
- "The imported event can't be recorded because its click occurred before this conversion's click-through window. Make sure you import the most recent data., at conversions[0].gclid",
71
- 'The imported event includes an order ID that was previously recorded, so the event was not processed., at conversions[0].order_id',
72
- 'The imported event could not be attributed to a click. This may be because the event did not come from a Google Ads campaign., at conversions[0].gclid',
73
- "The imported event can't be recorded because its click occurred before this conversion's click-through window. Make sure you import the most recent data., at conversions[0].conversion_action",
110
+ 'The imported event has a conversion_date_time that precedes the click. Make sure your conversion_date_time is correct and try again.',
111
+ "The click ID or call is associated with an Ads account you don't have access to. Make sure you import conversions for accounts managed by your manager account.",
112
+ "The imported event can't be recorded because its click occurred before this conversion's click-through window. Make sure you import the most recent data.",
113
+ 'The imported event includes an order ID that was previously recorded, so the event was not processed.',
114
+ 'The imported event could not be attributed to a click. This may be because the event did not come from a Google Ads campaign.',
115
+ "The imported event can't be recorded because its click occurred before this conversion's click-through window. Make sure you import the most recent data.",
74
116
  ];
75
117
 
76
118
  if (responseJson.partialFailureError) {
77
119
  utils.logMessage(1, responseJson.partialFailureError.message);
78
- if (!knownErrors.includes(responseJson.partialFailureError.message)) {
120
+ const trimmedMessage =
121
+ responseJson.partialFailureError.message.split('., at')[0] ?? '';
122
+ if (!knownErrors.includes(trimmedMessage)) {
79
123
  throw new Error(responseJson.partialFailureError.message);
80
124
  }
81
125
  } else if (responseJson.error) {
82
126
  throw new Error(responseJson.error.message);
83
127
  } else {
84
- utils.logEvent(this.source, event.event, event.region);
128
+ utils.logEvent(
129
+ this.source,
130
+ event.event,
131
+ event.region,
132
+ response.status,
133
+ requestOptions.body,
134
+ );
85
135
  }
86
136
  },
87
- };
88
137
 
89
- interface GoogleAdsResponse {
90
- partialFailureError?: {
91
- message: string;
92
- };
93
- error?: {
94
- message: string;
95
- };
96
- }
138
+ async getToken(config: GoogleAdsConfig) {
139
+ if (tokenCache && tokenCache.createdAt + 3500 * 1000 > Date.now()) {
140
+ return tokenCache;
141
+ }
142
+
143
+ if (!tokenPromise) {
144
+ tokenPromise = (async () => {
145
+ const tokenResponse = await fetch(
146
+ `https://oauth2.googleapis.com/token`,
147
+ {
148
+ method: 'POST',
149
+ headers: {
150
+ // eslint-disable-next-line @typescript-eslint/naming-convention
151
+ 'Content-Type': 'application/x-www-form-urlencoded',
152
+ },
153
+ body: new URLSearchParams({
154
+ // eslint-disable-next-line @typescript-eslint/naming-convention
155
+ client_id: config.client.id,
156
+ // eslint-disable-next-line @typescript-eslint/naming-convention
157
+ client_secret: config.client.secret,
158
+ // eslint-disable-next-line @typescript-eslint/naming-convention
159
+ refreshToken: config.refreshToken,
160
+ // eslint-disable-next-line @typescript-eslint/naming-convention
161
+ grant_type: 'refresh_token',
162
+ }),
163
+ },
164
+ );
165
+
166
+ if (!tokenResponse.ok) {
167
+ throw new Error('Access token fetch failed');
168
+ }
169
+
170
+ const parsedTokenResponse: TokenResponse = await tokenResponse.json();
171
+ const createdAt = Date.now();
172
+
173
+ tokenCache = { token: parsedTokenResponse.access_token, createdAt };
174
+ tokenPromise = null;
175
+ return tokenCache;
176
+ })();
177
+ }
178
+
179
+ return tokenPromise;
180
+ },
181
+ };
@@ -12,10 +12,7 @@ import { TypeSafeObject } from '../types/UtilTypes';
12
12
 
13
13
  let aiClient: appInsights.TelemetryClient | null = null;
14
14
 
15
- if (
16
- process.env.APPLICATIONINSIGHTS_CONNECTION_STRING ||
17
- process.env.APPINSIGHTS_INSTRUMENTATIONKEY
18
- ) {
15
+ if (process.env.APPLICATIONINSIGHTS_CONNECTION_STRING) {
19
16
  appInsights
20
17
  .setup() // Set environment variable APPLICATIONINSIGHTS_CONNECTION_STRING, so setup() can be called w/o parameters
21
18
  .setAutoCollectDependencies(false)
@@ -76,11 +73,15 @@ export function logEvent(
76
73
  publisher: string,
77
74
  event: string,
78
75
  region: string,
76
+ status: number,
77
+ payload?: string,
79
78
  ): void {
80
79
  const properties: Telemetry['properties'] = {
81
- publisher: publisher,
82
- event: event,
83
- region: region,
80
+ publisher,
81
+ event,
82
+ region,
83
+ status,
84
+ payload,
84
85
  };
85
86
  logMessage(1, 'Event dispatched to platform', properties);
86
87
  }
@@ -322,7 +323,10 @@ export async function handlePlatformRequest({
322
323
  headers: { ...requestOptions.headers },
323
324
  };
324
325
 
325
- const rawPlatformResponse = await fetch(fullUrl, fetchRequestOptions);
326
+ const rawPlatformResponse: Response = await fetch(
327
+ fullUrl,
328
+ fetchRequestOptions,
329
+ );
326
330
 
327
331
  if (process.env.PUBLISHER_TESTURL) {
328
332
  await handleTestRequest({
@@ -339,11 +343,23 @@ export async function handlePlatformRequest({
339
343
  event.event,
340
344
  );
341
345
 
342
- logEvent(source, event.event, event.region);
346
+ logEvent(
347
+ source,
348
+ event.event,
349
+ event.region,
350
+ rawPlatformResponse.status,
351
+ requestOptions.body ? JSON.stringify(requestOptions.body) : undefined,
352
+ );
343
353
 
344
354
  return processedPlatformResponse;
345
355
  } else {
346
- logEvent(source, event.event, event.region);
356
+ logEvent(
357
+ source,
358
+ event.event,
359
+ event.region,
360
+ rawPlatformResponse.status,
361
+ requestOptions.body ? JSON.stringify(requestOptions.body) : undefined,
362
+ );
347
363
  }
348
364
  }
349
365
 
@@ -377,16 +393,19 @@ async function handleTestRequest(params: HandleTestRequestParams) {
377
393
  }
378
394
  }
379
395
 
380
- export function parseErrorMessage(error: Error | Error['message']) {
381
- let errorMessage;
396
+ export function formatError(error: Error | Error['message']) {
397
+ let formattedError: Error;
382
398
  if (error instanceof Error) {
399
+ formattedError = error;
383
400
  if (error.cause) {
384
- errorMessage = `${error.message} Cause: ${error.cause.toString()}`;
401
+ formattedError.message = `${
402
+ error.message
403
+ } Cause: ${error.cause.toString()}`;
385
404
  } else {
386
- errorMessage = error.message;
405
+ formattedError.message = error.message;
387
406
  }
388
407
  } else {
389
- errorMessage = error;
408
+ formattedError = new Error(error);
390
409
  }
391
- return errorMessage;
410
+ return formattedError;
392
411
  }
@@ -1,7 +1,8 @@
1
+ const localSettings = require('../local.settings.json').Values;
2
+ process.env = { ...process.env, ...localSettings };
1
3
  const publisher = require('../dist/index.js').publisher.AffiliatePort;
2
4
  const defaultPublisher = require('../dist/index.js').DefaultPublisher;
3
5
  const utils = require('../dist/index.js').utils;
4
- var assert = require('assert');
5
6
  const testUtils = require('../dist/index.js').testUtils;
6
7
 
7
8
  describe('test AffiliatePort publisher', function () {
package/tests/CJ.js CHANGED
@@ -1,7 +1,8 @@
1
+ const localSettings = require('../local.settings.json').Values;
2
+ process.env = { ...process.env, ...localSettings };
1
3
  const publisher = require('../dist/index.js').publisher.CJ;
2
4
  const defaultPublisher = require('../dist/index.js').DefaultPublisher;
3
5
  const utils = require('../dist/index.js').utils;
4
- var assert = require('assert');
5
6
  const testUtils = require('../dist/index.js').testUtils;
6
7
 
7
8
  describe('test CJ publisher', function () {
package/tests/Facebook.js CHANGED
@@ -1,7 +1,8 @@
1
+ const localSettings = require('../local.settings.json').Values;
2
+ process.env = { ...process.env, ...localSettings };
1
3
  const publisher = require('../dist/index.js').publisher.Facebook;
2
4
  const defaultPublisher = require('../dist/index.js').DefaultPublisher;
3
5
  const utils = require('../dist/index.js').utils;
4
- var assert = require('assert');
5
6
  const testUtils = require('../dist/index.js').testUtils;
6
7
 
7
8
  describe('test Facebook publisher', function () {
@@ -1,9 +1,9 @@
1
+ const localSettings = require('../local.settings.json').Values;
2
+ process.env = { ...process.env, ...localSettings };
1
3
  const publisher = require('../dist/index.js').publisher.GoogleAds;
2
4
  const defaultPublisher = require('../dist/index.js').DefaultPublisher;
3
5
  const utils = require('../dist/index.js').utils;
4
6
  const testUtils = require('../dist/index.js').testUtils;
5
- const localSettings = require('../local.settings.json');
6
- process.env = { ...process.env, ...localSettings };
7
7
 
8
8
  describe('test GoogleAds publisher', function () {
9
9
  it('purchase', async function () {
@@ -84,11 +84,11 @@ function trigger(data) {
84
84
  },
85
85
  refreshToken:
86
86
  // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
87
- process.env['Publisher_GoogleAds_RefreshToken_' + data.event.region],
87
+ process.env.Publisher_GoogleAds_RefreshToken,
88
88
  customerId:
89
89
  // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
90
- process.env['Publisher_GoogleAds_CustomerID_' + data.event.region],
91
- conversionId: process.env.Publisher_GoogleAds_ConversionID_purchase_MAIN,
90
+ process.env.Publisher_GoogleAds_CustomerID,
91
+ conversionId: process.env.Publisher_GoogleAds_ConversionID_purchase,
92
92
  };
93
93
 
94
94
  switch (data.event?.code) {
@@ -1,7 +1,8 @@
1
+ const localSettings = require('../local.settings.json').Values;
2
+ process.env = { ...process.env, ...localSettings };
1
3
  const publisher = require('../dist/index.js').publisher.GoogleAnalytics4;
2
4
  const defaultPublisher = require('../dist/index.js').DefaultPublisher;
3
5
  const utils = require('../dist/index.js').utils;
4
- var assert = require('assert');
5
6
  const testUtils = require('../dist/index.js').testUtils;
6
7
 
7
8
  describe('test GoogleAnalytics4 publisher', function () {
package/tests/Piano.js CHANGED
@@ -1,8 +1,9 @@
1
+ const localSettings = require('../local.settings.json').Values;
2
+ process.env = { ...process.env, ...localSettings };
1
3
  const publisher = require('../dist/index.js').publisher.Piano;
2
4
  const defaultPublisher = require('../dist/index.js').DefaultPublisher;
3
5
  const utils = require('../dist/index.js').utils;
4
6
  const testUtils = require('../dist/index.js').testUtils;
5
- var assert = require('assert');
6
7
 
7
8
  describe('test Piano publisher', function () {
8
9
  it('purchase', async function () {
@@ -1,7 +1,8 @@
1
+ const localSettings = require('../local.settings.json').Values;
2
+ process.env = { ...process.env, ...localSettings };
1
3
  const publisher = require('../dist/index.js').publisher.UniversalAnalytics;
2
4
  const defaultPublisher = require('../dist/index.js').DefaultPublisher;
3
5
  const utils = require('../dist/index.js').utils;
4
- var assert = require('assert');
5
6
  const testUtils = require('../dist/index.js').testUtils;
6
7
 
7
8
  describe('test UniversalAnalytics publisher', function () {
@@ -1,66 +0,0 @@
1
- let tokenCache: TokenCache | null = null;
2
- let tokenPromise: Promise<TokenCache> | null = null;
3
-
4
- export async function fetchGoogleAdsToken(
5
- config: GoogleAdsConfig,
6
- ): Promise<TokenCache> {
7
- if (tokenCache && tokenCache.createdAt + 3500 * 1000 > Date.now()) {
8
- return tokenCache;
9
- }
10
-
11
- if (!tokenPromise) {
12
- tokenPromise = (async () => {
13
- const tokenResponse = await fetch(`https://oauth2.googleapis.com/token`, {
14
- method: 'POST',
15
- headers: {
16
- // eslint-disable-next-line @typescript-eslint/naming-convention
17
- 'Content-Type': 'application/x-www-form-urlencoded',
18
- },
19
- body: new URLSearchParams({
20
- // eslint-disable-next-line @typescript-eslint/naming-convention
21
- client_id: config.client.id,
22
- // eslint-disable-next-line @typescript-eslint/naming-convention
23
- client_secret: config.client.secret,
24
- refreshToken: config.refreshToken,
25
- // eslint-disable-next-line @typescript-eslint/naming-convention
26
- grant_type: 'refresh_token',
27
- }),
28
- });
29
-
30
- // if (!tokenResponse.ok) {
31
- // throw new Error('Access token fetch failed')
32
- // }
33
-
34
- const parsedTokenResponse: TokenResponse = await tokenResponse.json();
35
- const createdAt = Date.now();
36
-
37
- tokenCache = { token: parsedTokenResponse.access_token, createdAt };
38
- tokenPromise = null;
39
- return tokenCache;
40
- })();
41
- }
42
-
43
- return tokenPromise;
44
- }
45
-
46
- interface TokenCache {
47
- token: string;
48
- createdAt: number;
49
- }
50
-
51
- interface TokenResponse {
52
- // eslint-disable-next-line @typescript-eslint/naming-convention
53
- access_token: string;
54
- }
55
-
56
- export interface GoogleAdsConfig {
57
- client: {
58
- id: string;
59
- secret: string;
60
- developerToken: string;
61
- };
62
- refreshToken: string;
63
- customerId: string;
64
- conversionId: string;
65
- [x: string]: unknown;
66
- }