@mhubcloud/tms_core 1.7.6 → 2.0.0-beta.1

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 (44) 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/dist/index.js +609 -0
  8. package/index.js +1 -0
  9. package/{lib → js/lib}/defaultPublisher.js +26 -24
  10. package/js/lib/utils.js +349 -0
  11. package/js/publishers/AffiliatePort.js +78 -0
  12. package/{publishers → js/publishers}/CJ.js +7 -5
  13. package/{publishers → js/publishers}/Facebook.js +6 -4
  14. package/{publishers → js/publishers}/GoogleAnalytics4.js +8 -6
  15. package/js/publishers/HttpPublisher.js +64 -0
  16. package/{publishers → js/publishers}/Piano.js +6 -4
  17. package/{publishers → js/publishers}/UniversalAnalytics.js +7 -5
  18. package/package.json +46 -29
  19. package/releasePlan.md +3 -0
  20. package/src/defaultPublisher.ts +132 -0
  21. package/src/index.ts +25 -0
  22. package/src/publishers/affiliatePort.ts +69 -0
  23. package/src/publishers/cj.ts +67 -0
  24. package/src/publishers/facebook.ts +49 -0
  25. package/src/publishers/googleAnalytics4.ts +60 -0
  26. package/src/publishers/piano.ts +71 -0
  27. package/src/publishers/universalAnalytics.ts +63 -0
  28. package/src/types/EventHubMessage.ts +8 -0
  29. package/src/types/EventPublisher.ts +9 -0
  30. package/src/types/PlatformPublisher.ts +19 -0
  31. package/src/types/UtilTypes.ts +1 -0
  32. package/src/types/global.d.ts +3 -0
  33. package/src/utils/utils.ts +358 -0
  34. package/tests/AffiliatePort.js +107 -85
  35. package/tests/CJ.js +144 -119
  36. package/tests/Facebook.js +124 -103
  37. package/tests/GoogleAnalytics4.js +82 -74
  38. package/tests/Piano.js +112 -99
  39. package/tests/UniversalAnalytics.js +90 -77
  40. package/tests/testUtils.js +7 -0
  41. package/tsconfig.eslint.json +43 -0
  42. package/tsconfig.json +26 -0
  43. package/lib/utils.js +0 -307
  44. package/publishers/AffiliatePort.js +0 -61
@@ -0,0 +1,71 @@
1
+ import * as utils from './../utils/utils';
2
+ import {
3
+ PlatformPublisher,
4
+ PlatformRequestOptions,
5
+ } from '../types/PlatformPublisher';
6
+
7
+ interface Aux {
8
+ visitorId: string;
9
+ ref: string;
10
+ userAgent?: string;
11
+ referer?: string;
12
+ forwardedFor?: string;
13
+ }
14
+
15
+ interface PianoData {
16
+ aux: Aux;
17
+ }
18
+
19
+ export const piano: PlatformPublisher = {
20
+ source: 'Piano',
21
+ /**
22
+ * Publishes data to Piano
23
+ * @param log zure Function context.log
24
+ * @param config Publisher configuration
25
+ * @param data Data in publisher-friendly format to be published
26
+ * @param event Event for which the function is triggered
27
+ */
28
+ async publish(log, config, data, event) {
29
+ if (
30
+ typeof config.siteId === 'string' &&
31
+ typeof config.collectionDomain === 'string'
32
+ ) {
33
+ const pianoData = utils.removeUndefined(data) as PianoData;
34
+ let query = '';
35
+ // https://developers.atinternet-solutions.com/piano-analytics-tagging-en/event-tagging-piano-analytics-en/events-pattern-en/#post-request_5
36
+ // https://<collection-domain>/<path>?s=<site-id>&idclient=<visitor-id>&ref=<previous-url>
37
+ query = '?s=' + encodeURI(config.siteId);
38
+ query += '&idclient=' + encodeURI(pianoData.aux.visitorId);
39
+ query += '&ref=' + encodeURI(pianoData.aux.ref);
40
+
41
+ const requestOptions: PlatformRequestOptions = {
42
+ method: 'POST',
43
+ };
44
+
45
+ requestOptions.headers = {};
46
+
47
+ if (pianoData.aux.userAgent) {
48
+ requestOptions.headers['User-Agent'] = pianoData.aux.userAgent;
49
+ }
50
+ if (pianoData.aux.referer) {
51
+ requestOptions.headers.Referer = pianoData.aux.referer;
52
+ }
53
+ if (pianoData.aux.forwardedFor) {
54
+ requestOptions.headers['X-Forwarded-For'] = pianoData.aux.forwardedFor;
55
+ }
56
+
57
+ const baseUrl = `https://${config.collectionDomain}/`;
58
+
59
+ await utils.handlePlatformRequest({
60
+ source: this.source,
61
+ baseUrl,
62
+ query,
63
+ requestOptions,
64
+ event,
65
+ logger: log,
66
+ });
67
+ } else {
68
+ throw new Error('Invalid config for platform ' + this.source);
69
+ }
70
+ },
71
+ };
@@ -0,0 +1,63 @@
1
+ import * as utils from './../utils/utils';
2
+ import {
3
+ PlatformPublisher,
4
+ PlatformRequestOptions,
5
+ } from '../types/PlatformPublisher';
6
+ import { TypeSafeObject } from '../types/UtilTypes';
7
+
8
+ export const universalAnalytics: PlatformPublisher = {
9
+ source: 'UniversalAnalytics',
10
+ /**
11
+ * Publishes data to UniversalAnalytics
12
+ * @param log zure Function context.log
13
+ * @param config Publisher configuration
14
+ * @param data Data in publisher-friendly format to be published
15
+ * @param event Event for which the function is triggered
16
+ */
17
+ async publish(log, config, data, event) {
18
+ if (config.trackingId) {
19
+ const uaData = utils.removeUndefined(data) as TypeSafeObject;
20
+ uaData.v = 1;
21
+ uaData.tid = config.trackingId;
22
+
23
+ let payload = '';
24
+
25
+ for (const property in uaData) {
26
+ if (Object.prototype.hasOwnProperty.call(uaData, property)) {
27
+ if (payload) {
28
+ payload += '&';
29
+ }
30
+ if (uaData[property]) {
31
+ payload +=
32
+ encodeURI(property) + '=' + encodeURI(uaData[property] ?? '');
33
+ }
34
+ }
35
+ }
36
+
37
+ const requestOptions: PlatformRequestOptions = {
38
+ method: 'POST',
39
+ body: payload,
40
+ headers: {
41
+ // eslint-disable-next-line @typescript-eslint/naming-convention
42
+ 'Content-Type': 'application/x-www-form-urlencoded',
43
+ // eslint-disable-next-line @typescript-eslint/naming-convention
44
+ 'User-Agent': 'mHub Cloud',
45
+ },
46
+ };
47
+ const baseUrl = 'https://www.google-analytics.com/collect';
48
+
49
+ const query = `?${payload}`;
50
+
51
+ await utils.handlePlatformRequest({
52
+ source: this.source,
53
+ baseUrl,
54
+ query,
55
+ requestOptions,
56
+ event,
57
+ logger: log,
58
+ });
59
+ } else {
60
+ throw new Error('Invalid config for platform ' + this.source);
61
+ }
62
+ },
63
+ };
@@ -0,0 +1,8 @@
1
+ export interface EventHubMessage {
2
+ enqueuedAt: number;
3
+ event: {
4
+ code: string;
5
+ name?: string;
6
+ };
7
+ [x: string]: unknown;
8
+ }
@@ -0,0 +1,9 @@
1
+ import { EventHubMessage } from './EventHubMessage';
2
+
3
+ export interface EventPublisherBaseClass {
4
+ config: Record<string, unknown>;
5
+ }
6
+
7
+ export interface EventPublisher extends EventPublisherBaseClass {
8
+ map: (data: EventHubMessage) => Promise<Record<string, unknown>>;
9
+ }
@@ -0,0 +1,19 @@
1
+ import { HttpMethod, Logger } from '@azure/functions';
2
+ import { BodyInit } from 'node-fetch';
3
+ import { TypeSafeObject } from './UtilTypes';
4
+
5
+ export interface PlatformPublisher {
6
+ source: string;
7
+ publish: (
8
+ log: Logger,
9
+ config: TypeSafeObject,
10
+ data: TypeSafeObject | unknown[],
11
+ event: string,
12
+ ) => Promise<void>;
13
+ }
14
+
15
+ export interface PlatformRequestOptions {
16
+ method: HttpMethod | undefined;
17
+ body?: BodyInit | undefined;
18
+ headers?: Record<string, string>;
19
+ }
@@ -0,0 +1 @@
1
+ export type TypeSafeObject = Record<string, unknown>;
@@ -0,0 +1,3 @@
1
+ declare namespace globalThis {
2
+ function it(): unknown;
3
+ }
@@ -0,0 +1,358 @@
1
+ import * as crypto from 'crypto';
2
+ import { Logger } from '@azure/functions';
3
+ import * as appInsights from 'applicationinsights';
4
+ import {
5
+ SeverityLevel,
6
+ Telemetry,
7
+ } from 'applicationinsights/out/Declarations/Contracts';
8
+ import fetch, { RequestInit, Response } from 'node-fetch';
9
+ import { EventPublisherBaseClass } from '../types/EventPublisher';
10
+ import { PlatformRequestOptions } from '../types/PlatformPublisher';
11
+ import { TypeSafeObject } from '../types/UtilTypes';
12
+
13
+ let aiClient: appInsights.TelemetryClient | null = null;
14
+
15
+ if (process.env.APPINSIGHTS_INSTRUMENTATIONKEY) {
16
+ appInsights
17
+ .setup() // Set environment variable APPINSIGHTS_INSTRUMENTATIONKEY, so setup() can be called w/o parameters
18
+ .setAutoCollectDependencies(false)
19
+ .setAutoCollectConsole(true, true)
20
+ .setSendLiveMetrics(false)
21
+ .setAutoCollectRequests(false)
22
+ .setAutoCollectPerformance(false)
23
+ .setUseDiskRetryCaching(false)
24
+ .start();
25
+ aiClient = appInsights.defaultClient;
26
+ }
27
+
28
+ /**
29
+ * Base class for Event Publisher classes
30
+ * Facilitates registration (depracated) and configuration
31
+ */
32
+ export class EventPublisherBase implements EventPublisherBaseClass {
33
+ config;
34
+ constructor(config: TypeSafeObject) {
35
+ this.config = config;
36
+ }
37
+
38
+ /** @deprecated Please use a 'new Class(config)' notation. The .register() method will be deleted in future major version
39
+ * Registers a new instance of @see EventPublisherBase -derived class with array of publishers
40
+ * @param {EventPublisher[]} publishers Tha publishers array to register with
41
+ * @param {Object} config Configuration to pass to new instance
42
+ * Call this static method on @see EventPublisherBase -derived class.
43
+ * It will create its instance and pass @param config to its constructor.
44
+ * Than it will add resulting instance of @param publishers.
45
+ */
46
+ public static register(publishers: unknown[], config: TypeSafeObject) {
47
+ // ? Otestovat zpetnou kompatibilitu
48
+ publishers.push(
49
+ new this(
50
+ config,
51
+ ) /* Trick: Creates instance of the specific derived class `register` has been called on */,
52
+ );
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Logs message to Azure Function context and to Application Insights (if configured)
58
+ */
59
+ export function log(
60
+ log: Logger,
61
+ level: SeverityLevel,
62
+ message: string,
63
+ properties?: Telemetry['properties'],
64
+ ): void {
65
+ log.warn(message);
66
+ aiClient?.trackTrace({
67
+ message,
68
+ severity: level,
69
+ properties,
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Logs exception to Azure Function context and to Application Insights (if configured)
75
+ */
76
+ export function logException(log: Logger, exception: Error) {
77
+ log.warn(exception);
78
+ aiClient?.trackException({
79
+ exception,
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Creates a deep copy of given object removing any properties or array items with undefined value
85
+ * @param originalValue Object or array to copy. If value is neither array nor object it's returned as passed.
86
+ * @returns Deep copy of @param originalValue (providing array or object has been 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.
87
+ */
88
+ export function removeUndefined(
89
+ originalValue: TypeSafeObject | unknown[],
90
+ ): TypeSafeObject | unknown[] {
91
+ // ? Toto bych rad otestoval. Klidne unit testy.
92
+ if (originalValue instanceof Array) {
93
+ return removeUndefinedFromArray(originalValue);
94
+ } else if (originalValue instanceof Object) {
95
+ return removeUndefinedFromObject(originalValue);
96
+ } else {
97
+ return originalValue;
98
+ }
99
+ }
100
+
101
+ function removeUndefinedFromObject(object: TypeSafeObject) {
102
+ const resultObject: TypeSafeObject = {};
103
+ for (const property in object) {
104
+ if (
105
+ Object.prototype.hasOwnProperty.call(object, property) &&
106
+ object[property] !== undefined
107
+ ) {
108
+ resultObject[property] = removeUndefined(
109
+ object[property] as TypeSafeObject | unknown[],
110
+ );
111
+ }
112
+ }
113
+ return resultObject;
114
+ }
115
+
116
+ function removeUndefinedFromArray(array: unknown[]) {
117
+ const resultArray: unknown[] = [];
118
+ for (const item of array) {
119
+ if (item !== undefined) {
120
+ resultArray.push(removeUndefined(item as TypeSafeObject | unknown[]));
121
+ }
122
+ }
123
+ return resultArray;
124
+ }
125
+
126
+ /**
127
+ * Computes SHA-256 hash of an email string. If email is not a string, undefined is returned
128
+ */
129
+ export function encodeEmail(email: string) {
130
+ return computeSha256HashFromString(email);
131
+ }
132
+
133
+ /**
134
+ * Computes SHA-256 hash of any string. If provided value is not a string, undefined is returned
135
+ */
136
+ function computeSha256HashFromString(stringToHash: string) {
137
+ if (typeof stringToHash !== 'string') {
138
+ return undefined;
139
+ }
140
+ return crypto
141
+ .createHash('sha256')
142
+ .update(stringToHash.toLowerCase(), 'ascii')
143
+ .digest('hex')
144
+ .toLowerCase();
145
+ }
146
+
147
+ interface Ad {
148
+ adPlatform: string;
149
+ visitAt: number;
150
+ adCodes?: Record<string, string>;
151
+ }
152
+
153
+ interface AdCodeInfo {
154
+ code: string;
155
+ visitAt: number;
156
+ }
157
+
158
+ /**
159
+ * Extracts ad code from standardly structured event data (in legacy or new format)
160
+ * @param 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.
161
+ * @param adPlatform value of `data.ads.adPlatform` field to filter ad codes by platform (this is for new style data; ignored for legacy data)
162
+ * @param 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
163
+ * @param 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.
164
+ */
165
+ export function extractAdCode(
166
+ data: {
167
+ ads: Ad[];
168
+ },
169
+ adPlatform: string,
170
+ adCode: string,
171
+ format: ((code: AdCodeInfo, ad: Ad) => string | null | unknown) | null = null,
172
+ ): AdCodeInfo | null | string | unknown {
173
+ // ? Tady se udělalo hodně změn, protože se dal pryč legacy kód. Pro kontrolu je potřebná lepší znalost dat.
174
+ // ? Udelat unit test
175
+ const ad = data.ads
176
+ .filter((x) => {
177
+ return x.adPlatform === adPlatform;
178
+ })
179
+ .sort((a, b) => b.visitAt - a.visitAt)[0];
180
+ if (!ad) {
181
+ return null;
182
+ }
183
+ const code: string | null | undefined = ad.adCodes
184
+ ? ad.adCodes[adCode]
185
+ : null;
186
+ if (code) {
187
+ const returnValue: AdCodeInfo = { code: code, visitAt: ad.visitAt };
188
+ if (format) {
189
+ return format(returnValue, ad);
190
+ }
191
+ return returnValue;
192
+ }
193
+ return null;
194
+ }
195
+
196
+ /**
197
+ * Transforms an object to be flat, for instance for GA4
198
+ * @param event Event that is received
199
+ * @param separator Separator that will be used to unnest the structure. The default separator is underscore _
200
+ * @returns
201
+ */
202
+ export function flatten(
203
+ event: TypeSafeObject,
204
+ separator = '_',
205
+ ): TypeSafeObject {
206
+ // ? Tady je pro safety potřeba celou funkci vylepšit
207
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
208
+ return Object.assign(
209
+ {},
210
+ // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unsafe-argument
211
+ ...(function _flatten(
212
+ o: TypeSafeObject,
213
+ separator: string,
214
+ parent?: string,
215
+ ): any {
216
+ return [].concat(
217
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
218
+ ...Object.keys(o).map((key) => {
219
+ if (typeof o[key] === 'object' && o[key] !== null) {
220
+ const newParent = parent ? parent + separator + key : key;
221
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
222
+ return _flatten(o[key] as TypeSafeObject, separator, newParent);
223
+ } else {
224
+ const newKey = parent ? parent + separator + key : key;
225
+ return { [newKey]: o[key] };
226
+ }
227
+ }),
228
+ );
229
+ })(event, separator),
230
+ );
231
+ }
232
+
233
+ /**
234
+ * Enhances header before sending test request to secondary target (like Pipedream)
235
+ * @param headers Request header dictionary. This contains the original headers and will be ehnanced.
236
+ * @param origUrl Original URL the request has been sent to
237
+ * @param origRequest The entire original request options
238
+ * @param origResponse Response recievend from original request
239
+ */
240
+ export function prepareTestRequestHeaders(
241
+ headers: TypeSafeObject,
242
+ origUrl: string,
243
+ origRequest: TypeSafeObject,
244
+ origResponse: Response,
245
+ ) {
246
+ headers['x-mhub-orig-url'] = origUrl;
247
+ headers['x-mhub-orig-method'] = origRequest.method;
248
+ headers['x-mhub-orig-response-status-code'] = origResponse.status;
249
+ headers['x-mhub-orig-response-status-message'] = origResponse.statusText;
250
+ }
251
+
252
+ /**
253
+ * Handles HTTP response from node-fetch
254
+ * @param response Node-fetch response object
255
+ * @param event Event for which the response is
256
+ * @returns Deserialized response JSON
257
+ */
258
+ export async function handleResponse(response: Response, event: string) {
259
+ if (!response.ok) {
260
+ throw new Error(
261
+ `HTTP Error code: ${
262
+ response.status
263
+ }, event: ${event}, body: ${await response.text()}`,
264
+ );
265
+ }
266
+ const body = await response.text();
267
+ try {
268
+ return JSON.parse(body) as object;
269
+ } catch (ex) {
270
+ return body;
271
+ }
272
+ }
273
+
274
+ interface HandleRequestParams {
275
+ source: string;
276
+ baseUrl: string;
277
+ requestOptions: PlatformRequestOptions;
278
+ query?: string;
279
+ event: string;
280
+ logger: Logger;
281
+ }
282
+
283
+ /**
284
+ *
285
+ * @param params
286
+ * @returns
287
+ */
288
+
289
+ export async function handlePlatformRequest({
290
+ source,
291
+ baseUrl,
292
+ requestOptions,
293
+ query = '',
294
+ event,
295
+ logger,
296
+ }: HandleRequestParams) {
297
+ if (!baseUrl || baseUrl.length < 3) {
298
+ // ? Mohli bychom zacit delat poradnou kontrolu validity vstupnich paramteru. Idealne pres ZOD
299
+ throw new Error('URL for request was not provided or was not valid');
300
+ }
301
+ const fullUrl = baseUrl + query;
302
+
303
+ const fetchRequestOptions: RequestInit = {
304
+ method: requestOptions.method ?? 'POST',
305
+ body: requestOptions.body ?? undefined,
306
+ // eslint-disable-next-line @typescript-eslint/naming-convention
307
+ headers: { ...requestOptions.headers },
308
+ };
309
+
310
+ const rawPlatformResponse = await fetch(fullUrl, fetchRequestOptions);
311
+
312
+ if (process.env.PUBLISHER_TESTURL) {
313
+ await handleTestRequest({
314
+ fetchRequestOptions,
315
+ fullUrl,
316
+ query,
317
+ rawPlatformResponse,
318
+ });
319
+ }
320
+ const processedPlatfromResponse = await handleResponse(
321
+ rawPlatformResponse,
322
+ event,
323
+ );
324
+
325
+ log(logger, 1, `${source} - ${event} - sent`);
326
+
327
+ return processedPlatfromResponse;
328
+ }
329
+
330
+ interface HandleTestRequestParams {
331
+ fetchRequestOptions: RequestInit;
332
+ fullUrl: string;
333
+ query: string | undefined;
334
+ rawPlatformResponse: Response;
335
+ }
336
+
337
+ async function handleTestRequest(params: HandleTestRequestParams) {
338
+ try {
339
+ params.fetchRequestOptions.headers = {
340
+ ...params.fetchRequestOptions.headers,
341
+ // eslint-disable-next-line @typescript-eslint/naming-convention
342
+ 'x-mhub-orig-url': params.fullUrl,
343
+ // eslint-disable-next-line @typescript-eslint/naming-convention
344
+ 'x-mhub-orig-method': params.fetchRequestOptions.method ?? '',
345
+ // eslint-disable-next-line @typescript-eslint/naming-convention
346
+ 'x-mhub-orig-response-status-code': `${params.rawPlatformResponse.status}`,
347
+ // eslint-disable-next-line @typescript-eslint/naming-convention
348
+ 'x-mhub-orig-response-status-message':
349
+ params.rawPlatformResponse.statusText,
350
+ };
351
+ await fetch(
352
+ `${process.env.PUBLISHER_TESTURL ?? ''} + ${params.query ?? ''}`,
353
+ params.fetchRequestOptions,
354
+ );
355
+ } catch {
356
+ /* empty */
357
+ }
358
+ }