-
Notifications
You must be signed in to change notification settings - Fork 102
/
transform.js
290 lines (253 loc) · 9.28 KB
/
transform.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
/* eslint-disable camelcase */
/* eslint-disable @typescript-eslint/naming-convention */
const _ = require('lodash');
const { SHA256 } = require('crypto-js');
const get = require('get-value');
const set = require('set-value');
const { ConfigurationError, InstrumentationError } = require('@rudderstack/integrations-lib');
const { EventType } = require('../../../constants');
const {
constructPayload,
defaultRequestConfig,
defaultPostRequestConfig,
removeUndefinedAndNullValues,
defaultBatchRequestConfig,
getSuccessRespEvents,
isDefinedAndNotNullAndNotEmpty,
getDestinationExternalID,
getFieldValueFromMessage,
getHashFromArrayWithDuplicate,
handleRtTfSingleEventError,
batchMultiplexedEvents,
} = require('../../util');
const { process: processV2, processRouterDest: processRouterDestV2 } = require('./transformV2');
const { getContents } = require('./util');
const {
trackMapping,
TRACK_ENDPOINT,
BATCH_ENDPOINT,
eventNameMapping,
MAX_BATCH_SIZE,
PARTNER_NAME,
} = require('./config');
const { JSON_MIME_TYPE } = require('../../util/constant');
const USER_EMAIL_KEY_PATH = 'context.user.email';
const USER_PHONE_NUMBER_KEY_PATH = 'context.user.phone_number';
const checkContentType = (contents, contentType) => {
if (Array.isArray(contents)) {
contents.forEach((content) => {
if (!content.content_type) {
// eslint-disable-next-line no-param-reassign
content.content_type = contentType || 'product_group';
}
});
}
return contents;
};
const getTrackResponse = (message, Config, event) => {
const pixel_code = Config.pixelCode;
let payload = constructPayload(message, trackMapping);
// if contents is not an array
if (payload.properties?.contents && !Array.isArray(payload.properties.contents)) {
payload.properties.contents = [payload.properties.contents];
}
if (payload.properties && !payload.properties?.contents && message.properties?.products) {
// retreiving data from products only when contents is not present
payload.properties = {
...payload.properties,
contents: getContents(message),
};
}
if (payload.properties?.contents) {
payload.properties.contents = checkContentType(
payload.properties?.contents,
message.properties?.contentType,
);
}
const externalId = getDestinationExternalID(message, 'tiktokExternalId');
if (isDefinedAndNotNullAndNotEmpty(externalId)) {
set(payload, 'context.user.external_id', externalId);
}
const traits = getFieldValueFromMessage(message, 'traits');
// taking user properties like email and phone from traits
let email = get(payload, USER_EMAIL_KEY_PATH);
if (!isDefinedAndNotNullAndNotEmpty(email) && traits?.email) {
set(payload, USER_EMAIL_KEY_PATH, traits.email);
}
let phone_number = get(payload, USER_PHONE_NUMBER_KEY_PATH);
if (!isDefinedAndNotNullAndNotEmpty(phone_number) && traits?.phone) {
set(payload, USER_PHONE_NUMBER_KEY_PATH, traits.phone);
}
payload = { pixel_code, event, ...payload };
/*
* Hashing user related detail i.e external_id, email, phone_number
*/
if (Config.hashUserProperties) {
const external_id = get(payload, 'context.user.external_id');
if (isDefinedAndNotNullAndNotEmpty(external_id)) {
payload.context.user.external_id = SHA256(external_id.trim()).toString();
}
email = get(payload, USER_EMAIL_KEY_PATH);
if (isDefinedAndNotNullAndNotEmpty(email)) {
payload.context.user.email = SHA256(email.trim().toLowerCase()).toString();
}
phone_number = get(payload, USER_PHONE_NUMBER_KEY_PATH);
if (isDefinedAndNotNullAndNotEmpty(phone_number)) {
payload.context.user.phone_number = SHA256(phone_number.trim()).toString();
}
}
const response = defaultRequestConfig();
response.headers = {
'Access-Token': Config.accessToken,
'Content-Type': JSON_MIME_TYPE,
};
response.method = defaultPostRequestConfig.requestMethod;
response.endpoint = TRACK_ENDPOINT;
// add partner name
response.body.JSON = removeUndefinedAndNullValues({
...payload,
partner_name: PARTNER_NAME,
});
return response;
};
const trackResponseBuilder = async (message, { Config }) => {
const { eventsToStandard, sendCustomEvents } = Config;
if (!message.event || typeof message.event !== 'string') {
throw new InstrumentationError('Either event name is not present or it is not a string');
}
let event = message.event?.toLowerCase().trim();
const standardEventsMap = getHashFromArrayWithDuplicate(eventsToStandard);
if (!sendCustomEvents && eventNameMapping[event] === undefined && !standardEventsMap[event]) {
throw new InstrumentationError(
`Event name (${event}) is not valid, must be mapped to one of standard events`,
);
}
const responseList = [];
if (standardEventsMap[event]) {
Object.keys(standardEventsMap).forEach((key) => {
if (key === event) {
standardEventsMap[event].forEach((eventName) => {
responseList.push(getTrackResponse(message, Config, eventName));
});
}
});
return responseList;
}
// Doc https://ads.tiktok.com/help/article/standard-events-parameters?lang=en
// For custom event we do not want to lower case the event or trim it we just want to send those as it is
event = eventNameMapping[event] || message.event;
// if there exists no event mapping we will build payload with custom event recieved
responseList.push(getTrackResponse(message, Config, event));
return responseList;
};
const process = async (event) => {
const { message, destination } = event;
if (destination.Config?.version === 'v2') {
return processV2(event);
}
if (!destination.Config.accessToken) {
throw new ConfigurationError('Access Token not found. Aborting ');
}
if (!destination.Config.pixelCode) {
throw new ConfigurationError('Pixel Code not found. Aborting');
}
if (!message.type) {
throw new InstrumentationError('Event type is required');
}
const messageType = message.type.toLowerCase();
let response;
if (messageType === EventType.TRACK) {
response = await trackResponseBuilder(message, destination);
} else {
throw new InstrumentationError(`Event type ${messageType} is not supported`);
}
return response;
};
function batchEvents(eventsChunk) {
const { destination, events } = eventsChunk;
const { accessToken, pixelCode } = destination.Config;
const { batchedRequest } = defaultBatchRequestConfig();
const batchResponseList = [];
events.forEach((transformedEvent) => {
// extracting destination
// from the first event in a batch
const cloneTransformedEvent = _.clone(transformedEvent);
delete cloneTransformedEvent.body.JSON.pixel_code;
// Partner name must be added above "batch": [..]
delete cloneTransformedEvent.body.JSON.partner_name;
cloneTransformedEvent.body.JSON.type = 'track';
batchResponseList.push(cloneTransformedEvent.body.JSON);
});
batchedRequest.body.JSON = {
pixel_code: pixelCode,
partner_name: PARTNER_NAME,
batch: batchResponseList,
};
batchedRequest.endpoint = BATCH_ENDPOINT;
batchedRequest.headers = {
'Access-Token': accessToken,
'Content-Type': 'application/json',
};
return batchedRequest;
}
function getEventChunks(event, trackResponseList, eventsChunk) {
// only for already transformed payload
// eslint-disable-next-line no-param-reassign
event.message = Array.isArray(event.message) ? event.message : [event.message];
if (event.message[0].body.JSON.test_event_code) {
const { metadata, destination, message } = event;
trackResponseList.push(getSuccessRespEvents(message, [metadata], destination));
} else {
eventsChunk.push({
message: event.message,
metadata: event.metadata,
destination: event.destination,
});
}
}
const processRouterDest = async (inputs, reqMetadata) => {
const { destination } = inputs[0];
const { Config } = destination;
if (Config?.version === 'v2') {
return processRouterDestV2(inputs, reqMetadata);
}
const trackResponseList = []; // list containing single track event in batched format
const eventsChunk = []; // temporary variable to divide payload into chunks
const errorRespList = [];
await Promise.all(
inputs.map(async (event) => {
try {
if (event.message.statusCode) {
// already transformed event
getEventChunks(event, trackResponseList, eventsChunk);
} else {
// if not transformed
getEventChunks(
{
message: await process(event),
metadata: event.metadata,
destination: event.destination,
},
trackResponseList,
eventsChunk,
);
}
} catch (error) {
const errRespEvent = handleRtTfSingleEventError(event, error, reqMetadata);
errorRespList.push(errRespEvent);
}
}),
);
const batchedResponseList = [];
if (eventsChunk.length > 0) {
const batchedEvents = batchMultiplexedEvents(eventsChunk, MAX_BATCH_SIZE);
batchedEvents.forEach((batch) => {
const batchedRequest = batchEvents(batch);
batchedResponseList.push(
getSuccessRespEvents(batchedRequest, batch.metadata, batch.destination, true),
);
});
}
return [...batchedResponseList.concat(trackResponseList), ...errorRespList];
};
module.exports = { process, processRouterDest };