@mhubcloud/tms_core 3.4.0-beta.5 → 3.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhubcloud/tms_core",
3
- "version": "3.4.0-beta.5",
3
+ "version": "3.4.0",
4
4
  "description": "Core function for mHub Cloud TMS publishers",
5
5
  "homepage": "https://mhubcloud.com/",
6
6
  "license": "Apache 2.0",
@@ -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
- }