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

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.4",
3
+ "version": "3.4.0-beta.5",
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
@@ -10,7 +10,16 @@ let serviceBusClient: ServiceBusClient | undefined;
10
10
  let cosmosClient: CosmosClient | undefined;
11
11
  const senders: Record<string, ServiceBusSender> = {};
12
12
 
13
- export type PublisherConfig = Record<string, string[] | string[][]>;
13
+ type PublisherConfig = Record<
14
+ string,
15
+ string | [string, PublisherEvent[]] | PublisherEvent[]
16
+ >;
17
+
18
+ type PublisherEvent = Record<string, string>;
19
+ interface EventInfo {
20
+ code: string;
21
+ [key: string]: string;
22
+ }
14
23
 
15
24
  /**
16
25
  * Evaluates an event against publisher configurations
@@ -19,18 +28,21 @@ export type PublisherConfig = Record<string, string[] | string[][]>;
19
28
  * @returns list of publishers for which an event is relevant.
20
29
  */
21
30
  export function resolveEvent(
22
- eventCode: string,
31
+ event: EventInfo,
23
32
  publisherConfig?: PublisherConfig,
24
33
  ) {
25
34
  if (!publisherConfig) {
26
35
  const envConfig = process.env.Broker_PublisherConfig;
36
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
27
37
  publisherConfig = envConfig ? JSON.parse(envConfig) : {};
28
38
  }
29
39
 
30
40
  const publishers = [];
31
41
 
32
- for (const publisher of Object.keys(publisherConfig)) {
33
- const publisherEvents = publisherConfig[publisher];
42
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
43
+ for (const publisher of Object.keys(publisherConfig!)) {
44
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
45
+ const publisherEvents = publisherConfig![publisher];
34
46
  if (publisherEvents) {
35
47
  if (publisherEvents[0] === '*') {
36
48
  let matchesExcluded = false;
@@ -39,15 +51,12 @@ export function resolveEvent(
39
51
  Array.isArray(publisherEvents[1]) &&
40
52
  publisherEvents[1].length > 0
41
53
  ) {
42
- for (const event of publisherEvents[1]) {
54
+ for (const publisherEvent of publisherEvents[1]) {
43
55
  if (matchesExcluded) {
44
56
  break;
45
57
  }
46
58
 
47
- if (typeof event === 'string') {
48
- const regex = new RegExp(event);
49
- matchesExcluded = regex.test(eventCode);
50
- }
59
+ matchesExcluded = matchEvent(event, publisherEvent);
51
60
  }
52
61
  }
53
62
  if (!matchesExcluded) {
@@ -56,12 +65,11 @@ export function resolveEvent(
56
65
  } else {
57
66
  let match = false;
58
67
 
59
- for (const event of publisherEvents) {
60
- if (typeof event === 'string') {
61
- const regex = new RegExp(event);
62
- match = regex.test(eventCode);
68
+ for (const publisherEvent of publisherEvents) {
69
+ if (Array.isArray(publisherEvent)) {
70
+ continue;
63
71
  }
64
-
72
+ match = matchEvent(event, publisherEvent);
65
73
  if (match) {
66
74
  publishers.push(publisher);
67
75
  }
@@ -77,6 +85,35 @@ export function resolveEvent(
77
85
  return publishers;
78
86
  }
79
87
 
88
+ function matchEvent(event: EventInfo, publisherEvent: string | PublisherEvent) {
89
+ if (typeof publisherEvent === 'string') {
90
+ const regex = new RegExp(publisherEvent);
91
+ const match = regex.test(event.code);
92
+ return match;
93
+ } else if (typeof publisherEvent === 'object') {
94
+ let paramMatch = true;
95
+ for (const key of Object.keys(publisherEvent)) {
96
+ const publisherEventParam = publisherEvent[key];
97
+ const eventParam = event[key];
98
+ if (
99
+ typeof publisherEventParam === 'string' &&
100
+ typeof eventParam === 'string'
101
+ ) {
102
+ const regex = new RegExp(publisherEventParam);
103
+ paramMatch = regex.test(eventParam);
104
+ if (!paramMatch) {
105
+ return false;
106
+ }
107
+ } else {
108
+ throw new Error('Unexpected formatting in publisher configuration');
109
+ }
110
+ }
111
+ return true;
112
+ } else {
113
+ throw new Error('Unexpected formatting in publisher configuration');
114
+ }
115
+ }
116
+
80
117
  /**
81
118
  * Sends an event to Service Bus
82
119
  * @param message event to be sent
@@ -100,6 +137,7 @@ export async function dispatchEvent(
100
137
  if (senders[queue]) {
101
138
  sender = senders[queue];
102
139
  } else {
140
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
103
141
  senders[queue] = serviceBusClient!.createSender(queue);
104
142
  sender = senders[queue];
105
143
  }
@@ -40,23 +40,17 @@ 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 formattedError = utils.formatError(result.reason);
44
- utils.logException(
45
- azureFunctionContext.log,
46
- new Error(formattedError.message),
47
- );
48
- throw formattedError;
43
+ const errorMessage = utils.parseErrorMessage(result.reason);
44
+ utils.logException(azureFunctionContext.log, new Error(errorMessage));
45
+ throw new Error(errorMessage);
49
46
  } else {
50
- const formattedError = utils.formatError(
47
+ const errorMessage = utils.parseErrorMessage(
51
48
  typeof result.reason === 'object'
52
49
  ? JSON.stringify(result.reason)
53
50
  : (result.reason as Error['message']),
54
51
  );
55
- utils.logException(
56
- azureFunctionContext.log,
57
- new Error(formattedError.message),
58
- );
59
- throw formattedError;
52
+ utils.logException(azureFunctionContext.log, new Error(errorMessage));
53
+ throw new Error(errorMessage);
60
54
  }
61
55
  }
62
56
  }
@@ -1,65 +1,24 @@
1
- import { Logger } from '@azure/functions';
2
1
  import * as utils from './../utils/utils';
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
- }
2
+ import {
3
+ PlatformPublisher,
4
+ PlatformRequestOptions,
5
+ } from '../types/PlatformPublisher';
6
+ import {
7
+ fetchGoogleAdsToken,
8
+ GoogleAdsConfig,
9
+ } from '../utils/tokens/googleAdsToken';
16
10
 
17
11
  interface GoogleAdsData {
18
12
  gclid?: string;
19
13
  wbraid?: string;
20
14
  gbraid?: string;
21
15
  // eslint-disable-next-line @typescript-eslint/naming-convention
22
- user_identifiers?: Record<string, unknown>[];
16
+ user_identifiers: Record<string, unknown>[];
23
17
  conversionAction?: string;
24
18
  [x: string]: unknown;
25
19
  }
26
20
 
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 = {
21
+ export const googleAds: PlatformPublisher = {
63
22
  source: 'GoogleAds',
64
23
  /**
65
24
  *Publishes data to Google Ads
@@ -79,7 +38,7 @@ export const googleAds: GoogleAdsPlatformPublisher = {
79
38
  return;
80
39
  }
81
40
 
82
- const tokenData = await this.getToken(config);
41
+ const tokenData = await fetchGoogleAdsToken(config);
83
42
 
84
43
  data.conversionAction = `customers/${config.customerId}/conversionActions/${config.conversionId}`;
85
44
 
@@ -95,7 +54,6 @@ export const googleAds: GoogleAdsPlatformPublisher = {
95
54
  headers: {
96
55
  // eslint-disable-next-line @typescript-eslint/naming-convention
97
56
  Authorization: `Bearer ${tokenData.token}`,
98
- // eslint-disable-next-line @typescript-eslint/naming-convention
99
57
  'developer-token': config.client.developerToken,
100
58
  },
101
59
  body: JSON.stringify(body),
@@ -107,75 +65,32 @@ export const googleAds: GoogleAdsPlatformPublisher = {
107
65
  const responseJson: GoogleAdsResponse = await response.json();
108
66
 
109
67
  const knownErrors = [
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.",
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",
116
74
  ];
117
75
 
118
76
  if (responseJson.partialFailureError) {
119
77
  utils.logMessage(1, responseJson.partialFailureError.message);
120
- const trimmedMessage =
121
- responseJson.partialFailureError.message.split('., at')[0] ?? '';
122
- if (!knownErrors.includes(trimmedMessage)) {
78
+ if (!knownErrors.includes(responseJson.partialFailureError.message)) {
123
79
  throw new Error(responseJson.partialFailureError.message);
124
80
  }
125
81
  } else if (responseJson.error) {
126
82
  throw new Error(responseJson.error.message);
127
83
  } else {
128
- utils.logEvent(
129
- this.source,
130
- event.event,
131
- event.region,
132
- response.status,
133
- requestOptions.body,
134
- );
135
- }
136
- },
137
-
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
- })();
84
+ utils.logEvent(this.source, event.event, event.region);
177
85
  }
178
-
179
- return tokenPromise;
180
86
  },
181
87
  };
88
+
89
+ interface GoogleAdsResponse {
90
+ partialFailureError?: {
91
+ message: string;
92
+ };
93
+ error?: {
94
+ message: string;
95
+ };
96
+ }
@@ -0,0 +1,66 @@
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
+ }
@@ -12,7 +12,10 @@ import { TypeSafeObject } from '../types/UtilTypes';
12
12
 
13
13
  let aiClient: appInsights.TelemetryClient | null = null;
14
14
 
15
- if (process.env.APPLICATIONINSIGHTS_CONNECTION_STRING) {
15
+ if (
16
+ process.env.APPLICATIONINSIGHTS_CONNECTION_STRING ||
17
+ process.env.APPINSIGHTS_INSTRUMENTATIONKEY
18
+ ) {
16
19
  appInsights
17
20
  .setup() // Set environment variable APPLICATIONINSIGHTS_CONNECTION_STRING, so setup() can be called w/o parameters
18
21
  .setAutoCollectDependencies(false)
@@ -73,15 +76,11 @@ export function logEvent(
73
76
  publisher: string,
74
77
  event: string,
75
78
  region: string,
76
- status: number,
77
- payload?: string,
78
79
  ): void {
79
80
  const properties: Telemetry['properties'] = {
80
- publisher,
81
- event,
82
- region,
83
- status,
84
- payload,
81
+ publisher: publisher,
82
+ event: event,
83
+ region: region,
85
84
  };
86
85
  logMessage(1, 'Event dispatched to platform', properties);
87
86
  }
@@ -323,10 +322,7 @@ export async function handlePlatformRequest({
323
322
  headers: { ...requestOptions.headers },
324
323
  };
325
324
 
326
- const rawPlatformResponse: Response = await fetch(
327
- fullUrl,
328
- fetchRequestOptions,
329
- );
325
+ const rawPlatformResponse = await fetch(fullUrl, fetchRequestOptions);
330
326
 
331
327
  if (process.env.PUBLISHER_TESTURL) {
332
328
  await handleTestRequest({
@@ -343,23 +339,11 @@ export async function handlePlatformRequest({
343
339
  event.event,
344
340
  );
345
341
 
346
- logEvent(
347
- source,
348
- event.event,
349
- event.region,
350
- rawPlatformResponse.status,
351
- requestOptions.body ? JSON.stringify(requestOptions.body) : undefined,
352
- );
342
+ logEvent(source, event.event, event.region);
353
343
 
354
344
  return processedPlatformResponse;
355
345
  } else {
356
- logEvent(
357
- source,
358
- event.event,
359
- event.region,
360
- rawPlatformResponse.status,
361
- requestOptions.body ? JSON.stringify(requestOptions.body) : undefined,
362
- );
346
+ logEvent(source, event.event, event.region);
363
347
  }
364
348
  }
365
349
 
@@ -393,19 +377,16 @@ async function handleTestRequest(params: HandleTestRequestParams) {
393
377
  }
394
378
  }
395
379
 
396
- export function formatError(error: Error | Error['message']) {
397
- let formattedError: Error;
380
+ export function parseErrorMessage(error: Error | Error['message']) {
381
+ let errorMessage;
398
382
  if (error instanceof Error) {
399
- formattedError = error;
400
383
  if (error.cause) {
401
- formattedError.message = `${
402
- error.message
403
- } Cause: ${error.cause.toString()}`;
384
+ errorMessage = `${error.message} Cause: ${error.cause.toString()}`;
404
385
  } else {
405
- formattedError.message = error.message;
386
+ errorMessage = error.message;
406
387
  }
407
388
  } else {
408
- formattedError = new Error(error);
389
+ errorMessage = error;
409
390
  }
410
- return formattedError;
391
+ return errorMessage;
411
392
  }
@@ -1,8 +1,7 @@
1
- const localSettings = require('../local.settings.json').Values;
2
- process.env = { ...process.env, ...localSettings };
3
1
  const publisher = require('../dist/index.js').publisher.AffiliatePort;
4
2
  const defaultPublisher = require('../dist/index.js').DefaultPublisher;
5
3
  const utils = require('../dist/index.js').utils;
4
+ var assert = require('assert');
6
5
  const testUtils = require('../dist/index.js').testUtils;
7
6
 
8
7
  describe('test AffiliatePort publisher', function () {
package/tests/CJ.js CHANGED
@@ -1,8 +1,7 @@
1
- const localSettings = require('../local.settings.json').Values;
2
- process.env = { ...process.env, ...localSettings };
3
1
  const publisher = require('../dist/index.js').publisher.CJ;
4
2
  const defaultPublisher = require('../dist/index.js').DefaultPublisher;
5
3
  const utils = require('../dist/index.js').utils;
4
+ var assert = require('assert');
6
5
  const testUtils = require('../dist/index.js').testUtils;
7
6
 
8
7
  describe('test CJ publisher', function () {
package/tests/Facebook.js CHANGED
@@ -1,8 +1,7 @@
1
- const localSettings = require('../local.settings.json').Values;
2
- process.env = { ...process.env, ...localSettings };
3
1
  const publisher = require('../dist/index.js').publisher.Facebook;
4
2
  const defaultPublisher = require('../dist/index.js').DefaultPublisher;
5
3
  const utils = require('../dist/index.js').utils;
4
+ var assert = require('assert');
6
5
  const testUtils = require('../dist/index.js').testUtils;
7
6
 
8
7
  describe('test Facebook publisher', function () {
@@ -1,9 +1,9 @@
1
- const localSettings = require('../local.settings.json').Values;
2
- process.env = { ...process.env, ...localSettings };
3
1
  const publisher = require('../dist/index.js').publisher.GoogleAds;
4
2
  const defaultPublisher = require('../dist/index.js').DefaultPublisher;
5
3
  const utils = require('../dist/index.js').utils;
6
4
  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,
87
+ process.env['Publisher_GoogleAds_RefreshToken_' + data.event.region],
88
88
  customerId:
89
89
  // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
90
- process.env.Publisher_GoogleAds_CustomerID,
91
- conversionId: process.env.Publisher_GoogleAds_ConversionID_purchase,
90
+ process.env['Publisher_GoogleAds_CustomerID_' + data.event.region],
91
+ conversionId: process.env.Publisher_GoogleAds_ConversionID_purchase_MAIN,
92
92
  };
93
93
 
94
94
  switch (data.event?.code) {
@@ -1,8 +1,7 @@
1
- const localSettings = require('../local.settings.json').Values;
2
- process.env = { ...process.env, ...localSettings };
3
1
  const publisher = require('../dist/index.js').publisher.GoogleAnalytics4;
4
2
  const defaultPublisher = require('../dist/index.js').DefaultPublisher;
5
3
  const utils = require('../dist/index.js').utils;
4
+ var assert = require('assert');
6
5
  const testUtils = require('../dist/index.js').testUtils;
7
6
 
8
7
  describe('test GoogleAnalytics4 publisher', function () {
package/tests/Piano.js CHANGED
@@ -1,9 +1,8 @@
1
- const localSettings = require('../local.settings.json').Values;
2
- process.env = { ...process.env, ...localSettings };
3
1
  const publisher = require('../dist/index.js').publisher.Piano;
4
2
  const defaultPublisher = require('../dist/index.js').DefaultPublisher;
5
3
  const utils = require('../dist/index.js').utils;
6
4
  const testUtils = require('../dist/index.js').testUtils;
5
+ var assert = require('assert');
7
6
 
8
7
  describe('test Piano publisher', function () {
9
8
  it('purchase', async function () {
@@ -1,8 +1,7 @@
1
- const localSettings = require('../local.settings.json').Values;
2
- process.env = { ...process.env, ...localSettings };
3
1
  const publisher = require('../dist/index.js').publisher.UniversalAnalytics;
4
2
  const defaultPublisher = require('../dist/index.js').DefaultPublisher;
5
3
  const utils = require('../dist/index.js').utils;
4
+ var assert = require('assert');
6
5
  const testUtils = require('../dist/index.js').testUtils;
7
6
 
8
7
  describe('test UniversalAnalytics publisher', function () {