@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
@@ -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
+ }
@@ -1,116 +1,138 @@
1
- const publisher = require("../index.js").publisher.AffiliatePort;
2
- const defaultPublisher = require("../index.js").DefaultPublisher;
3
- const utils = require("../index.js").utils;
1
+ const publisher = require('../dist/index.js').publisher.AffiliatePort;
2
+ const defaultPublisher = require('../dist/index.js').DefaultPublisher;
3
+ const utils = require('../dist/index.js').utils;
4
4
  var assert = require('assert');
5
+ const testUtils = require('./testUtils');
5
6
 
6
7
  describe('test AffiliatePort publisher', function () {
7
8
  it('purchase', async function () {
8
9
  var eventHubMessages = [
9
10
  {
10
- "event": {
11
- "region": "smarty.cz",
12
- "id": "825333270-1011.1659006863812",
13
- "code": "purchase"
11
+ event: {
12
+ region: 'smarty.cz',
13
+ id: '825333270-1011.1659006863812',
14
+ code: 'purchase',
14
15
  },
15
- "user": {
16
- "ts": "customer.HL",
17
- "email": "jan.brzobohaty@crossmasters.com",
18
- "id": "a2992fb8-40ac-4b88-9cf5-7fba02f1f611"
16
+ user: {
17
+ ts: 'customer.HL',
18
+ email: 'jan.brzobohaty@crossmasters.com',
19
+ id: 'a2992fb8-40ac-4b88-9cf5-7fba02f1f611',
19
20
  },
20
- "session": {
21
- "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15",
22
- "ip": "217.195.174.253"
21
+ session: {
22
+ userAgent:
23
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15',
24
+ ip: '217.195.174.253',
23
25
  },
24
- "permissions": [],
25
- "currencyCode": "CZK",
26
- "publisher": {
27
- "code": "www.smarty.cz",
28
- "tm": "GTM-NVNTFG",
29
- "tmv": "193"
26
+ permissions: [],
27
+ currencyCode: 'CZK',
28
+ publisher: {
29
+ code: 'www.smarty.cz',
30
+ tm: 'GTM-NVNTFG',
31
+ tmv: '193',
30
32
  },
31
- "$schema": "https://schema.measurementhub.net/mhub/2021-12-27/purchase",
32
- "id": "3511111111",
33
- "revenue": 10,
34
- "products": [
33
+ $schema: 'https://schema.measurementhub.net/mhub/2021-12-27/purchase',
34
+ id: '3511111111',
35
+ revenue: 10,
36
+ products: [
35
37
  {
36
- "id": "67137",
37
- "price": 10,
38
- "quantity": 1,
39
- "category": "Playstation",
40
- "masterType": "Konzole"
41
- },{
42
- "id": "67137",
43
- "price": 20,
44
- "quantity": 1,
45
- "category": "Monitor",
46
- "masterType": "Video"
47
- }
38
+ id: '67137',
39
+ price: 10,
40
+ quantity: 1,
41
+ category: 'Playstation',
42
+ masterType: 'Konzole',
43
+ },
44
+ {
45
+ id: '67137',
46
+ price: 20,
47
+ quantity: 1,
48
+ category: 'Monitor',
49
+ masterType: 'Video',
50
+ },
51
+ ],
52
+ location: 'https://www.smarty.cz/checkoutSuccess',
53
+ referrer:
54
+ 'https://www.smarty.cz/Order/AddReservation?productId=57873&shopAddressId=24798',
55
+ enqueuedAt: 1659006864038,
56
+ ads: [
57
+ {
58
+ adPlatform: 'affiliateport',
59
+ adCodes: {
60
+ transaction_id:
61
+ '816f5670dbf52c3dc27a7e849903c327d705ef643993fbab3',
62
+ },
63
+ visitAt: 1658392668000,
64
+ },
48
65
  ],
49
- "location": "https://www.smarty.cz/checkoutSuccess",
50
- "referrer": "https://www.smarty.cz/Order/AddReservation?productId=57873&shopAddressId=24798",
51
- "enqueuedAt": 1659006864038,
52
- "ads": [{"adPlatform":"affiliateport","adCodes":{"transaction_id":"816f5670dbf52c3dc27a7e849903c327d705ef643993fbab3"},"visitAt":1658392668000}],
53
- "EventProcessedUtcTime": "2022-07-28T11:54:50.2528883Z",
54
- "PartitionId": 0,
55
- "EventEnqueuedUtcTime": "2022-07-28T11:14:24.1170000Z"
56
- }
66
+ EventProcessedUtcTime: '2022-07-28T11:54:50.2528883Z',
67
+ PartitionId: 0,
68
+ EventEnqueuedUtcTime: '2022-07-28T11:14:24.1170000Z',
69
+ },
57
70
  ];
58
71
  await test(eventHubMessages);
59
72
  });
60
- })
61
-
73
+ });
62
74
 
63
75
  /**
64
76
  * Test function
65
77
  * @param [object] object obsahující nastavení testovací parametry
66
78
  */
67
79
  async function test(eventHubMessages) {
68
- try{
69
- await defaultPublisher(console, eventHubMessages, "AffiliatePort", publisher, trigger);
70
- }catch(err){
71
- console.log(err);
72
- }
80
+ try {
81
+ await defaultPublisher(
82
+ testUtils.context,
83
+ eventHubMessages,
84
+ 'AffiliatePort',
85
+ publisher,
86
+ trigger,
87
+ );
88
+ } catch (err) {
89
+ console.log(err);
90
+ }
73
91
  }
74
92
 
75
93
  function trigger(data) {
76
- const publishers = [];
77
- let publisher;
78
-
79
- //Dummy config, replace with your own
80
- const config = {
81
- offerId: 123,
82
- };
94
+ const publishers = [];
95
+ let publisher;
83
96
 
84
- switch (data.event?.code) {
85
- case "purchase":
86
- publisher = AffiliatePortPurchasePublisher;
87
- publisher.register(publishers, config);
88
- break;
89
- }
90
- return publishers;
91
- };
97
+ // Dummy config, replace with your own
98
+ const config = {
99
+ offerId: 123,
100
+ };
101
+
102
+ switch (data.event?.code) {
103
+ case 'purchase':
104
+ publisher = AffiliatePortPurchasePublisher;
105
+ publisher.register(publishers, config);
106
+ break;
107
+ }
108
+ return publishers;
109
+ }
92
110
 
93
111
  /**
94
112
  * Implements Purchase event publisher for Facebook
95
113
  * @implements {EventPublisher<{id: string, access_token: string}>}
96
114
  */
97
115
  class AffiliatePortPurchasePublisher extends utils.EventPublisherBase {
98
- /**
99
- * Maps `purchase` event data to AffiliatePort payload
100
- * @param {Object} data `purchase` event data
101
- * @returns {Object} Data mapped from incomming Event Hub message to platform payload
102
- */
103
- map(data) {
104
- return {
105
- orderId: data.id,
106
- revenue: data.revenue, //should be without VAT and shipping, payment fees
107
- products: data.products.map(pr => ({
108
- quantity: pr.quantity, // e.g. 1
109
- price: pr.price, // e.g. 109
110
- category: pr.category.replace(/ /g, "-"), // e.g. Playstation
111
- masterType: pr.masterType // e.g. Konzole
112
- })),
113
- transactionID: utils.extractAdCode(data, 'affiliateport', 'transaction_id')?.code // AffiliatePort identificator taken automatically from URL
114
- };
115
- }
116
- };
116
+ /**
117
+ * Maps `purchase` event data to AffiliatePort payload
118
+ * @param {Object} data `purchase` event data
119
+ * @returns {Object} Data mapped from incomming Event Hub message to platform payload
120
+ */
121
+ map(data) {
122
+ return {
123
+ orderId: data.id,
124
+ revenue: data.revenue, // should be without VAT and shipping, payment fees
125
+ products: data.products.map((pr) => ({
126
+ quantity: pr.quantity, // e.g. 1
127
+ price: pr.price, // e.g. 109
128
+ category: pr.category.replace(/ /g, '-'), // e.g. Playstation
129
+ masterType: pr.masterType, // e.g. Konzole
130
+ })),
131
+ transactionID: utils.extractAdCode(
132
+ data,
133
+ 'affiliateport',
134
+ 'transaction_id',
135
+ )?.code, // AffiliatePort identificator taken automatically from URL
136
+ };
137
+ }
138
+ }