@mhubcloud/tms_core 1.7.6 → 2.0.0-beta.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.
Files changed (43) hide show
  1. package/.eslintignore +3 -0
  2. package/.eslintrc.cjs +87 -0
  3. package/.prettierignore +3 -0
  4. package/.prettierrc +10 -0
  5. package/.vscode/extensions.json +12 -0
  6. package/.vscode/settings.json +52 -0
  7. package/index.js +1 -0
  8. package/{lib → js/lib}/defaultPublisher.js +26 -24
  9. package/js/lib/utils.js +349 -0
  10. package/js/publishers/AffiliatePort.js +78 -0
  11. package/{publishers → js/publishers}/CJ.js +7 -5
  12. package/{publishers → js/publishers}/Facebook.js +6 -4
  13. package/{publishers → js/publishers}/GoogleAnalytics4.js +8 -6
  14. package/js/publishers/HttpPublisher.js +64 -0
  15. package/{publishers → js/publishers}/Piano.js +6 -4
  16. package/{publishers → js/publishers}/UniversalAnalytics.js +7 -5
  17. package/package.json +46 -29
  18. package/releasePlan.md +3 -0
  19. package/src/defaultPublisher.ts +132 -0
  20. package/src/index.ts +25 -0
  21. package/src/publishers/affiliatePort.ts +69 -0
  22. package/src/publishers/cj.ts +67 -0
  23. package/src/publishers/facebook.ts +49 -0
  24. package/src/publishers/googleAnalytics4.ts +60 -0
  25. package/src/publishers/piano.ts +71 -0
  26. package/src/publishers/universalAnalytics.ts +63 -0
  27. package/src/types/EventHubMessage.ts +8 -0
  28. package/src/types/EventPublisher.ts +9 -0
  29. package/src/types/PlatformPublisher.ts +19 -0
  30. package/src/types/UtilTypes.ts +1 -0
  31. package/src/types/global.d.ts +3 -0
  32. package/src/utils/utils.ts +358 -0
  33. package/tests/AffiliatePort.js +107 -85
  34. package/tests/CJ.js +144 -119
  35. package/tests/Facebook.js +124 -103
  36. package/tests/GoogleAnalytics4.js +82 -74
  37. package/tests/Piano.js +112 -99
  38. package/tests/UniversalAnalytics.js +90 -77
  39. package/tests/testUtils.js +7 -0
  40. package/tsconfig.eslint.json +43 -0
  41. package/tsconfig.json +26 -0
  42. package/lib/utils.js +0 -307
  43. package/publishers/AffiliatePort.js +0 -61
package/.eslintignore ADDED
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ build/
3
+ dist/
package/.eslintrc.cjs ADDED
@@ -0,0 +1,87 @@
1
+ module.exports = {
2
+ root: true,
3
+ plugins: ['@typescript-eslint', 'prettier'],
4
+ extends: [
5
+ 'eslint:recommended',
6
+ 'plugin:@typescript-eslint/recommended',
7
+ 'plugin:import/recommended',
8
+ 'plugin:import/typescript',
9
+ 'plugin:prettier/recommended',
10
+ 'plugin:@typescript-eslint/recommended-requiring-type-checking',
11
+ 'plugin:@typescript-eslint/strict',
12
+ ],
13
+ parser: '@typescript-eslint/parser',
14
+ parserOptions: {
15
+ ecmaVersion: 'latest',
16
+ sourceType: 'module',
17
+ project: ['./tsconfig.eslint.json'],
18
+ },
19
+ env: {
20
+ node: true,
21
+ es6: true,
22
+ es2021: true,
23
+ },
24
+ settings: {
25
+ 'import/resolver': {
26
+ // https://github.com/import-js/eslint-plugin-import#typescript
27
+ typescript: true,
28
+ node: true,
29
+ },
30
+ },
31
+ rules: {
32
+ 'no-nested-ternary': 'error', // https://eslint.org/docs/latest/rules/no-nested-ternary
33
+ 'spaced-comment': ['error', 'always'], // https://eslint.org/docs/latest/rules/spaced-comment
34
+ curly: 'error', // https://eslint.org/docs/latest/rules/curly
35
+ 'import/order': [
36
+ // Enforce a convention in module import order
37
+ // https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md
38
+ 'error',
39
+ {
40
+ groups: [
41
+ 'type',
42
+ 'builtin',
43
+ 'external',
44
+ 'internal',
45
+ ['parent', 'sibling', 'index'],
46
+ 'unknown',
47
+ ],
48
+ 'newlines-between': 'never',
49
+ alphabetize: {
50
+ order: 'asc',
51
+ caseInsensitive: true,
52
+ },
53
+ warnOnUnassignedImports: true,
54
+ },
55
+ ],
56
+ // TypeScript specific rules
57
+ '@typescript-eslint/method-signature-style': 'error', // https://typescript-eslint.io/rules/method-signature-style/
58
+ '@typescript-eslint/naming-convention': [
59
+ // https://typescript-eslint.io/rules/naming-convention/
60
+ 'error',
61
+ {
62
+ selector: 'default',
63
+ format: ['camelCase'],
64
+ leadingUnderscore: 'forbid',
65
+ trailingUnderscore: 'forbid',
66
+ },
67
+ {
68
+ selector: 'variable',
69
+ format: ['camelCase', 'UPPER_CASE'],
70
+ },
71
+ {
72
+ selector: 'parameter',
73
+ format: ['camelCase'],
74
+ leadingUnderscore: 'allow',
75
+ filter: {
76
+ regex: '^(_req|_res|_next)$',
77
+ match: true, // We need this for Express _req to work
78
+ },
79
+ },
80
+ {
81
+ selector: 'typeLike',
82
+ format: ['PascalCase'],
83
+ },
84
+ ],
85
+ },
86
+ ignorePatterns: ['.eslintrc.cjs', 'build_scripts/post-build.mjs', 'build_scripts/web-app-post-db-generate.mjs']
87
+ };
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ build/
3
+ dist/
package/.prettierrc ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/prettierrc",
3
+ "arrowParens": "always",
4
+ "singleQuote": true,
5
+ "jsxSingleQuote": true,
6
+ "useTabs": false,
7
+ "trailingComma": "all",
8
+ "singleAttributePerLine": true,
9
+ "endOfLine": "auto"
10
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
3
+ // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
4
+ // List of extensions which should be recommended for users of this workspace.
5
+ "recommendations": [
6
+ "dbaeumer.vscode-eslint"
7
+ ],
8
+ // List of extensions recommended by VS Code that should not be recommended for users of this workspace.
9
+ "unwantedRecommendations": [
10
+ "esbenp.prettier-vscode"
11
+ ]
12
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ // EDITOR
3
+ // ----------------------------------------
4
+ "[javascript]": {
5
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
6
+ },
7
+ "[json]": {
8
+ "editor.defaultFormatter": "vscode.json-language-features"
9
+ },
10
+ "[jsonc]": {
11
+ "editor.defaultFormatter": "vscode.json-language-features"
12
+ },
13
+ "[typescript]": {
14
+ "editor.autoClosingBrackets": "always",
15
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
16
+ },
17
+ "editor.codeActionsOnSave": [
18
+ // "source.organizeImports",
19
+ "source.fixAll.eslint"
20
+ ],
21
+ // "editor.formatOnSave": true,
22
+ "typescript.tsdk": "node_modules/typescript/lib",
23
+ // ESLINT
24
+ // ----------------------------------------
25
+ "eslint.alwaysShowStatus": true,
26
+ "eslint.options": {
27
+ "extensions": [
28
+ ".js",
29
+ ".cjs",
30
+ ".ts",
31
+ ".tsx"
32
+ ]
33
+ },
34
+ "eslint.validate": [
35
+ "javascript",
36
+ "typescript"
37
+ ],
38
+ "eslint.format.enable": true,
39
+ // FILES
40
+ // ----------------------------------------
41
+ "files.exclude": {
42
+ "**/*.log": true,
43
+ "**/*.log*": true,
44
+ "**/build": true,
45
+ "**/dist": true
46
+ },
47
+ "search.exclude": {
48
+ "**/node_modules": false
49
+ },
50
+ "volar.completion.preferredTagNameCase": "pascal",
51
+ "eslint.packageManager": "pnpm"
52
+ }
package/index.js CHANGED
@@ -10,6 +10,7 @@ module.exports = {
10
10
  GoogleAnalytics4 : require('./publishers/GoogleAnalytics4'),
11
11
  CJ : require('./publishers/CJ'),
12
12
  UniversalAnalytics : require('./publishers/UniversalAnalytics'),
13
+ HttpPublisher : require('./publishers/HttpPublisher'),
13
14
  AffiliatePort : require('./publishers/AffiliatePort')
14
15
  }
15
16
  }
@@ -3,26 +3,26 @@ const utils = require("./utils");
3
3
 
4
4
  /**
5
5
  * Implements the Azure Function, called for set of events received via Event Hub
6
- * @param {FunctionContext} context Azure Function context
6
+ * @param {FunctionContext} log Azure Function context.log.warn
7
7
  * @param {Object[]} eventHubMessages Array of messages received from Event Hub
8
8
  * @param {string} source Name of the publisher (it's folder name in which it's implemented)
9
9
  * @param {PlatformPublisher} publisher Publisher implementation instance (from <publisher>/publisher.js)
10
10
  * @param {Trigger} trigger Publisher-specific trigger function (from <publisher>/customizable/trigger.js)
11
11
  * @returns {Promise<void|ReqResTestData[]>} When run in test mode (@param context.test is true), returns collected array of requests (otherwise) sent to platform. When additionaly run as test with publication (@param context.tests and @param context.testPublish) the response contains both - request and response data.
12
12
  */
13
- module.exports = async function DefaultPublisher(context, eventHubMessages, source, publisher, trigger) {
14
- context.log(`JavaScript eventhub trigger function called for message array [${eventHubMessages.length}]`);
15
- global.console.log = context.log.warn || context.log;
13
+ module.exports = async function DefaultPublisher(azureFunctionContext, eventHubMessages, source, publisher, trigger) {
14
+
15
+ azureFunctionContext.log.warn(`JavaScript eventhub trigger function called for message array [${eventHubMessages.length}]`);
16
16
  let mi = 0;
17
17
 
18
18
  let processMessagePromises = [];
19
19
  for (const message of eventHubMessages) {
20
- processMessagePromises.push(processMessage(message, context, source, publisher, trigger, mi++));
20
+ processMessagePromises.push(processMessage(message, azureFunctionContext.log.warn, azureFunctionContext.test, azureFunctionContext.debug, azureFunctionContext.testPublish, source, publisher, trigger, mi++));
21
21
  }
22
22
  let results = await Promise.allSettled(processMessagePromises);
23
23
  for (const result of results) {
24
24
  if(result.status === "rejected"){
25
- utils.logException(result.reason);
25
+ utils.logException(azureFunctionContext.log.warn, result.reason);
26
26
  }
27
27
  }
28
28
  }
@@ -30,17 +30,19 @@ module.exports = async function DefaultPublisher(context, eventHubMessages, sour
30
30
  /**
31
31
  * Processes single message
32
32
  * @param {Object} message A message received from Event Hub
33
- * @param {FunctionContext} context Azure Function context
33
+ * @param {FunctionContext} log Azure Function context.log.warn
34
+ * @param {FunctionContext} test Azure Function context.test
34
35
  * @param {string} source Name of the publisher (it's folder name in which it's implemented)
35
36
  * @param {PlatformPublisher} platfromPublisher Publisher implementation instance (from <publisher>/publisher.js)
36
37
  * @param {Trigger} trigger Publisher-specific trigger function (from <publisher>/customizable/trigger.js)
37
38
  * @param {number} mi Messae index (0-based), only for logging purposes
38
39
  * @returns {Promise<boolean|ReqResTestData[]>} In normal mode returns true/false indicating if the message has been successfully published to any platform. In (unit) test mode returns array of publication requests (or even results if it's not unit test), fulse just in case the were no publishers configured.
39
40
  */
40
- async function processMessage(message, context, source, platfromPublisher, trigger, mi) {
41
- let ret = context.test ? [] : false;
42
- if (context.debug)
43
- context.log(`Processing message №${mi + 1} ${JSON.stringify(message, null, "")}`);
41
+ async function processMessage(message, log, test, debug, testPublish, source, platfromPublisher, trigger, mi) {
42
+
43
+ let ret = test ? [] : false;
44
+ if (debug)
45
+ log(`Processing message №${mi + 1} ${JSON.stringify(message, null, "")}`);
44
46
 
45
47
  const data = platfromPublisher.parse(message, context);
46
48
  const logProp = {
@@ -51,7 +53,7 @@ async function processMessage(message, context, source, platfromPublisher, trigg
51
53
  /** @type {EventPublisher[]} */
52
54
  const publishers = trigger(data);
53
55
  if (!publishers?.length) {
54
- utils.log2(context, 1, "Message excluded by filter (no publishers determined)", logProp);
56
+ utils.log2(log, 1, "Message excluded by filter (no publishers determined)", logProp);
55
57
  return false;
56
58
  }
57
59
 
@@ -64,7 +66,7 @@ async function processMessage(message, context, source, platfromPublisher, trigg
64
66
  if (prevPublisher === eventPublisher ? prevValidionResult : (prevValidionResult = eventPublisher.validate(data))) { //Optimization
65
67
  passedAnyValidation = true;
66
68
  } else {
67
- utils.log2(context, 1, "Message excluded by validation", logProp);
69
+ utils.log2(log, 1, "Message excluded by validation", logProp);
68
70
  continue;
69
71
  }
70
72
 
@@ -72,18 +74,18 @@ async function processMessage(message, context, source, platfromPublisher, trigg
72
74
 
73
75
  prevPublisher = eventPublisher;
74
76
 
75
- const res = await publishMessage(mapped, eventPublisher.config, context, platfromPublisher, message.event.code + (message.event.name ? "/"+ message.event.name : ""));
77
+ const res = await publishMessage(log, mapped, eventPublisher.config, test, testPublish, platfromPublisher, message.event.code + (message.event.name ? "/"+ message.event.name : ""));
76
78
  if (res) published = true;
77
- if (context.test && res instanceof Array) ret.push(...res);
78
- if (res && !context.test) ret = true;
79
+ if (test && res instanceof Array) ret.push(...res);
80
+ if (res && !test) ret = true;
79
81
  }
80
82
 
81
83
  if (!passedAnyValidation) {
82
- utils.log2(context, 2, "Message excluded by all publishers' validations", logProp);
84
+ utils.log2(log, 2, "Message excluded by all publishers' validations", logProp);
83
85
  return false;
84
86
  }
85
87
  if (!published) {
86
- utils.log2(context, 2, "The message has not been published to any publisher configuration", logProp);
88
+ utils.log2(log, 2, "The message has not been published to any publisher configuration", logProp);
87
89
  return false;
88
90
  }
89
91
 
@@ -94,18 +96,18 @@ async function processMessage(message, context, source, platfromPublisher, trigg
94
96
  * Publishes a message to configured publisher
95
97
  * @param {Object} mapped Mapped message data
96
98
  * @param {Object} config Platform-specific platform publisher configuration
97
- * @param {FunctionContext} context Azure Function context
99
+ * @param {FunctionContext} test Azure Function context.test
98
100
  * @param {PlatformPublisher} platfromPublisher Publisher implementation instance (from <publisher>/publisher.js)
99
101
  * @param {string} event for which logic was triggered
100
102
  * @returns {Promise<boolean|ReqResTestData[]>} In normal mode returns true/false indicating if the message has been successfully published to any platform. In (unit) test mode returns array of publication requests (or even results if it's not unit test), fulse just in case the were no publishers configured.
101
103
  */
102
- async function publishMessage(mapped, config, context, platfromPublisher, event) {
103
- let ret = context.test ? [] : true;
104
- if (context.test)
104
+ async function publishMessage(log, mapped, config, test, testPublish, platfromPublisher, event) {
105
+ let ret = test ? [] : true;
106
+ if (test)
105
107
  ret.push({ req: mapped, config: config });
106
108
 
107
- if (!context.test || context.testPublish) {
108
- const res = await platfromPublisher.publish(config, mapped, event);
109
+ if (!test || testPublish) {
110
+ const res = await platfromPublisher.publish(log, config, mapped, event);
109
111
  if (context.test) ret[ret.length - 1].res = res;
110
112
  return ret; //true
111
113
  } else {
@@ -0,0 +1,349 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const https = require('https');
4
+ const Ajv = require('ajv');
5
+
6
+ let aiClient;
7
+ if (process.env.APPINSIGHTS_INSTRUMENTATIONKEY) {
8
+ const appInsights = require('applicationinsights');
9
+ appInsights
10
+ .setup() // Set einvironmet variable APPINSIGHTS_INSTRUMENTATIONKEY, so setup() can be called w/o parameters
11
+ .setAutoCollectDependencies(false)
12
+ .setAutoCollectConsole(true, true)
13
+ .setSendLiveMetrics(false)
14
+ .setAutoCollectRequests(false)
15
+ .setAutoCollectPerformance(false)
16
+ .setUseDiskRetryCaching(false)
17
+ .start();
18
+ aiClient = appInsights.defaultClient;
19
+ }
20
+
21
+ /**
22
+ * Base class for Event Publisher classes
23
+ * Facilitates registration and configuration
24
+ */
25
+ class EventPublisherBase {
26
+ /**
27
+ * Create a new insatnce of the @see EventPublisherBase class
28
+ * @param {Object} config Publisher configuration (in platform-specific format)
29
+ */
30
+ constructor(config) {
31
+ this.config = config;
32
+ }
33
+
34
+ /**
35
+ * Validates input data that they can be passed to mapper
36
+ * @param {Object} data Input data to validate
37
+ * @returns {boolean} Validation result
38
+ */
39
+ validate(data) {
40
+ return true; // Override this method in custom publisher for custom validation
41
+ }
42
+
43
+ /**
44
+ * Registers a new instance of @see EventPublisherBase -derived class with array of publishers
45
+ * @param {EventPublisher[]} publishers Tha publishers array to register with
46
+ * @param {Object} config Configuration to pass to new instance
47
+ * Call this static method on @see EventPublisherBase -derived class.
48
+ * It will create its instance and pass @param config to its constructor.
49
+ * Than it will add resulting instance of @param publishers.
50
+ */
51
+ static register(publishers, config) {
52
+ publishers.push(
53
+ new this(
54
+ config,
55
+ ) /* Trick: Creates instance of the specific derived class `register` has been called on */,
56
+ );
57
+ }
58
+ }
59
+
60
+ module.exports = {
61
+ EventPublisherBase: EventPublisherBase,
62
+
63
+ /**
64
+ * Provides access to Application Insights logging (event reporting)
65
+ * @type {appInsights.TelemetryClient|null}
66
+ */
67
+ appInsights: aiClient || null,
68
+
69
+ /**
70
+ * Computes SHA-256 hash of a string
71
+ * @param {string} email The vaue to hash
72
+ * @returns {string} @param email SHA-256 hash; returns email if it is 𝑓𝑎𝑙𝑠𝑒 (null, undefined etc.)
73
+ */
74
+ encodeEmail: function encodeEmail(email) {
75
+ if (!email) {
76
+ return email;
77
+ }
78
+ return crypto
79
+ .createHash('sha256')
80
+ .update(email.toLowerCase(), 'ascii')
81
+ .digest('hex')
82
+ .toLowerCase();
83
+ },
84
+
85
+ /**
86
+ * Handles HTTP response from node-fetch
87
+ * @param {*} response Node-fetch response object
88
+ * @param {string} event for which response is
89
+ * @returns {Object} Deserialized response JSON
90
+ * Throws exception in case of response error code
91
+ */
92
+ handleResponse: async function handleResponse(response, event) {
93
+ if (!response.ok) {
94
+ throw new Error(
95
+ `HTTP Error code: ${
96
+ response.status
97
+ }, event: ${event}, body: ${await response.text()}`,
98
+ );
99
+ }
100
+ const body = await response.text();
101
+ try {
102
+ return JSON.parse(body);
103
+ } catch (ex) {
104
+ return body;
105
+ }
106
+ },
107
+
108
+ /**
109
+ * Creates a deep copy of given object removing any properties or array items with undefined value
110
+ * @param {*} obj Objec or array to copy. If value is neither array nor object it's returned as passed.
111
+ * @returns Deep copy of @param obj (providing array or object has bee passed in). Any properties or array item with undefined value are skipped when making the copy, recursively. Returns @param obj untouched if neither array nor object has been passed in.
112
+ */
113
+ removeUndefined: function removeUndefined(obj) {
114
+ let ret;
115
+ if (obj instanceof Array) {
116
+ ret = [];
117
+ for (let i = 0; i < obj.length; i++) {
118
+ if (obj[i] !== undefined) {
119
+ ret.push(removeUndefined(obj[i]));
120
+ }
121
+ }
122
+ } else if (obj instanceof Object) {
123
+ ret = {};
124
+ for (const prp in obj) {
125
+ if (obj.hasOwnProperty(prp) && obj[prp] !== undefined) {
126
+ ret[prp] = removeUndefined(obj[prp]);
127
+ }
128
+ }
129
+ } else {
130
+ return obj;
131
+ }
132
+ return ret;
133
+ },
134
+
135
+ /**
136
+ * Validates an object against the schema it states or given schema
137
+ * @param {Object} data An object to validate. The shema must be provided in its `$schema` property if ot passed in @param schema
138
+ * @param {string} schemaSource Name of a folder to look for schemas. Schemas JSON format are looked for in subfolder `customizable/schemas` of given folder.
139
+ * @param {string} schema Full URL id (i.e. URL starting with https://schema.measurementhub.net/), or just relative part of that URL to resolve schem in @param schemaSource`/customizable/schemas`
140
+ * @returns {boolean} Validation result, true if successful, Exception if unsuccessfull
141
+ */
142
+ validate: function validate(data, schemaSource, schema = null) {
143
+ schema = schema || data.$schema;
144
+ if (!schema) {
145
+ throw "Data doesn't have schema and schema not passed";
146
+ }
147
+ if (
148
+ !schema.startsWith('https://schema.measurementhub.net/') &&
149
+ (schema.startsWith('http://') || schema.startsWith('https://'))
150
+ ) {
151
+ throw `Schema ${data.$schema} is not supported, only https://schema.measurementhub.net/... and schemas identified by relative URL shemas are supported`;
152
+ }
153
+ let parts = schema.split('/');
154
+ if (parts.includes('..')) {
155
+ // Directory traversal attack protection
156
+ throw `It's invalid for schema URI to contain '..' segment, such as in ${schema}`;
157
+ }
158
+ if (schema.startsWith('http://') || schema.startsWith('https://')) {
159
+ parts = parts.slice(3);
160
+ }
161
+
162
+ const schemaLocation = `${schemaSource}/customizable/schemas/${parts.join(
163
+ '/',
164
+ )}.json`;
165
+ if (!fs.existsSync(schemaLocation)) {
166
+ throw `Schema file ${schemaLocation} not found`;
167
+ }
168
+
169
+ let schemaStr;
170
+ try {
171
+ schemaStr = fs.readFileSync(schemaLocation).toString();
172
+ } catch (ex) {
173
+ throw `Failed to load schema ${schema}`;
174
+ }
175
+
176
+ let schemaObj;
177
+ try {
178
+ schemaObj = JSON.parse(schemaStr);
179
+ } catch (ex) {
180
+ throw `Failed to deserialize ${schema}`;
181
+ }
182
+
183
+ const ajv = new Ajv({ allErrors: true });
184
+ let validate;
185
+ try {
186
+ validate = ajv.compile(schemaObj);
187
+ } catch (ex) {
188
+ throw `Failed to compile schema ${schema}`;
189
+ }
190
+
191
+ const valid = validate(data);
192
+ if (!valid) {
193
+ throw `Schema vaidation failed for schema ${schema}, ${
194
+ validate.errors.length
195
+ } errors: ${JSON.stringify(validate.errors)}`;
196
+ }
197
+ return true;
198
+ },
199
+
200
+ /**
201
+ * Enhances header before sending test request to secondary target (like Pipedream)
202
+ * @param {Object} headers Request header dictionary. This contains the original headers and will be ehnanced.
203
+ * @param {string} origUrl Original URL the request has been sent to
204
+ * @param {*} origRequest The entire original request options
205
+ * @param {*} origResponse Response recievend from original request
206
+ */
207
+ prepareTestRequestHeaders: function prepareTestRequestHeaders(
208
+ headers,
209
+ origUrl,
210
+ origRequest,
211
+ origResponse,
212
+ ) {
213
+ headers['x-mhub-orig-url'] = origUrl;
214
+ headers['x-mhub-orig-method'] = origRequest.method;
215
+ headers['x-mhub-orig-response-status-code'] = origResponse.status;
216
+ headers['x-mhub-orig-response-status-message'] = origResponse.statusText;
217
+ },
218
+
219
+ /**
220
+ * Extracts ad code from standardly structured event data (in legacy or new format)
221
+ * @param {{ads:(LegacyAd|Ad)[]}} data Event data to extract ad code from. As you can see the `ads` field is accepted in 2 formats the first being legacy and the later being new one.
222
+ * @param {string} adPlatform value of `data.ads.adPlatform` field to filter ad codes by platform (this is for new style data; ignored for legacy data)
223
+ * @param {string} adCode value of `data.ads.adType` field to filter items in `data.ads` by (this is for legacy style data); als used for new style data to identify the main parameter from `data.ads.adCodes` to obtain
224
+ * @param {(code: AdCodeInfo, ad: LegacyAd|Ad) => string|any} format If this custom function is provided it forats the code returned, so the function does not return object but string formated by this function. But nothing prevents it from returning object if the consumer can hanlde it.
225
+ * @returns {AdCodeInfo|string|null|any} Ad code and time when it was recorded of newest event (by `visitAt`) with given ad code from @param data.ads; when @param format is provided returns string result of that function.
226
+ */
227
+ extractAdCode: function extractAdCode(
228
+ data,
229
+ adPlatform,
230
+ adCode,
231
+ format = null,
232
+ ) {
233
+ const ad = data.ads
234
+ .filter(
235
+ (x) =>
236
+ x.adType === adCode /* legacy*/ ||
237
+ x.adPlatform === adPlatform /* new*/,
238
+ )
239
+ .sort((a, b) => b.visitAt - a.visitAt)[0];
240
+ const code =
241
+ ad?.adCode /* legacy*/ || (ad?.adCodes ? ad.adCodes[adCode] : null);
242
+ if (code) {
243
+ const ret = { code: code, visitAt: ad.visitAt };
244
+ if (format) {
245
+ return format(ret, ad);
246
+ }
247
+ return ret;
248
+ }
249
+ return null;
250
+ },
251
+
252
+ /**
253
+ * Logs message to Azure Function context and to Application Insights (if configured)
254
+ * @param {FunctionContext} log Azure Function context.log.warn
255
+ * @param {0|1|2|3|4} level Application Insights log level according to https://docs.microsoft.com/en-us/dotnet/api/microsoft.applicationinsights.datacontracts.severitylevel?view=azure-dotnet
256
+ * @param {string} message The message to log
257
+ * @param {Object} properties Properties to pass to Application Insights trace
258
+ */
259
+ log2: function log2(log, level, message, properties) {
260
+ log(message);
261
+ aiClient?.trackTrace({
262
+ message: message,
263
+ severityLevel: level,
264
+ properties: properties,
265
+ });
266
+ },
267
+
268
+ /**
269
+ * Logs exception to Azure Function context and to Application Insights (if configured)
270
+ * @param {FunctionContext} log Azure Function context.log.warn
271
+ * @param {object} exception
272
+ */
273
+ logException: function logException(log, exception) {
274
+ log(exception);
275
+ aiClient?.trackException({ exception: exception });
276
+ // raise exception if it is running under mocha test
277
+ if (typeof global.it === 'function') {
278
+ throw exception;
279
+ }
280
+ },
281
+
282
+ /**
283
+ * For http request. This function is for edge cases. Use Fetch if possible.
284
+ * @param {object} params (hostname, port, path, method)
285
+ * @param {object} postData
286
+ * @returns
287
+ */
288
+ httpRequest: function httpRequest(params, postData) {
289
+ return new Promise(function (resolve, reject) {
290
+ var req = https.request(params, function (res) {
291
+ // reject on bad status
292
+ if (res.statusCode < 200 || res.statusCode >= 300) {
293
+ return reject(new Error('statusCode=' + res.statusCode));
294
+ }
295
+ // cumulate data
296
+ var body = [];
297
+ res.on('data', function (chunk) {
298
+ body.push(chunk);
299
+ });
300
+ // resolve on end
301
+ res.on('end', function () {
302
+ try {
303
+ if (body.length > 0) {
304
+ body = JSON.parse(Buffer.concat(body).toString());
305
+ }
306
+ } catch (e) {
307
+ reject(e);
308
+ }
309
+ resolve(body);
310
+ });
311
+ });
312
+ // reject on request error
313
+ req.on('error', function (err) {
314
+ // This is not a "Second reject", just a different sort of failure
315
+ reject(err);
316
+ });
317
+ if (postData) {
318
+ req.write(postData);
319
+ }
320
+ // IMPORTANT
321
+ req.end();
322
+ });
323
+ },
324
+
325
+ /**
326
+ * Transforms an object to be flat, for instance for GA4
327
+ * @param {object} event Event that is received
328
+ * @param {sring} separator Separator that will be used to unnest the structure. The default separator is underscore _
329
+ * @returns
330
+ */
331
+ flatten: function flatten(event, separator) {
332
+ return Object.assign(
333
+ {},
334
+ ...(function _flatten(o, separator = '_', parent) {
335
+ return [].concat(
336
+ ...Object.keys(o).map((key) => {
337
+ if (typeof o[key] === 'object' && o[key] !== null) {
338
+ const newParent = parent ? parent + separator + key : key;
339
+ return _flatten(o[key], separator, newParent);
340
+ } else {
341
+ const newKey = parent ? parent + separator + key : key;
342
+ return { [newKey]: o[key] };
343
+ }
344
+ }),
345
+ );
346
+ })(event, separator),
347
+ );
348
+ },
349
+ };