forked from snowplow/snowplow-javascript-tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpayload.ts
More file actions
259 lines (232 loc) · 8.11 KB
/
Copy pathpayload.ts
File metadata and controls
259 lines (232 loc) · 8.11 KB
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
/*
* Copyright (c) 2022 Snowplow Analytics Ltd, 2010 Anthon Pang
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { base64urlencode, base64urldecode } from './base64';
import type { SelfDescribingJson, SelfDescribingJsonArray } from './core';
/**
* Type for a Payload dictionary
*/
export type Payload = Record<string, unknown>;
/**
* A tuple which represents the unprocessed JSON to be added to the Payload
*/
export type EventJsonWithKeys = { keyIfEncoded: string; keyIfNotEncoded: string; json: Record<string, unknown> };
/**
* An array of tuples which represents the unprocessed JSON to be added to the Payload
*/
export type EventJson = Array<EventJsonWithKeys>;
/**
* A function which will processor the Json onto the injected PayloadBuilder
*/
export type JsonProcessor = (
payloadBuilder: PayloadBuilder,
jsonForProcessing: EventJson,
contextEntitiesForProcessing: SelfDescribingJson[]
) => void;
/**
* Interface for mutable object encapsulating tracker payload
*/
export interface PayloadBuilder {
/**
* Adds an entry to the Payload
* @param key - Key for Payload dictionary entry
* @param value - Value for Payload dictionaty entry
*/
add: (key: string, value: unknown) => void;
/**
* Merges a payload into the existing payload
* @param dict - The payload to merge
*/
addDict: (dict: Payload) => void;
/**
* Caches a JSON object to be added to payload on build
* @param keyIfEncoded - key if base64 encoding is enabled
* @param keyIfNotEncoded - key if base64 encoding is disabled
* @param json - The json to be stringified and added to the payload
*/
addJson: (keyIfEncoded: string, keyIfNotEncoded: string, json: Record<string, unknown>) => void;
/**
* Caches a context entity to be added to payload on build
* @param entity - Context entity to add to the event
*/
addContextEntity: (entity: SelfDescribingJson) => void;
/**
* Gets the current payload, before cached JSON is processed
*/
getPayload: () => Payload;
/**
* Gets all JSON objects added to payload
*/
getJson: () => EventJson;
/**
* Adds a function which will be executed when building
* the payload to process the JSON which has been added to this payload
* @param jsonProcessor - The JsonProcessor function for this builder
*/
withJsonProcessor: (jsonProcessor: JsonProcessor) => void;
/**
* Builds and returns the Payload
* @param base64Encode - configures if unprocessed, cached json should be encoded
*/
build: () => Payload;
}
export function payloadBuilder(): PayloadBuilder {
const dict: Payload = {},
allJson: EventJson = [],
jsonForProcessing: EventJson = [],
contextEntitiesForProcessing: SelfDescribingJson[] = [];
let processor: JsonProcessor | undefined;
const add = (key: string, value: unknown): void => {
if (value != null && value !== '') {
// null also checks undefined
dict[key] = value;
}
};
const addDict = (dict: Payload): void => {
for (const key in dict) {
if (Object.prototype.hasOwnProperty.call(dict, key)) {
add(key, dict[key]);
}
}
};
const addJson = (keyIfEncoded: string, keyIfNotEncoded: string, json?: Record<string, unknown>): void => {
if (json && isNonEmptyJson(json)) {
const jsonWithKeys = { keyIfEncoded, keyIfNotEncoded, json };
jsonForProcessing.push(jsonWithKeys);
allJson.push(jsonWithKeys);
}
};
const addContextEntity = (entity: SelfDescribingJson): void => {
contextEntitiesForProcessing.push(entity);
};
return {
add,
addDict,
addJson,
addContextEntity,
getPayload: () => dict,
getJson: () => allJson,
withJsonProcessor: (jsonProcessor) => {
processor = jsonProcessor;
},
build: function () {
processor?.(this, jsonForProcessing, contextEntitiesForProcessing);
return dict;
},
};
}
/**
* A helper to build a Snowplow request from a set of name-value pairs, provided using the add methods.
* Will base64 encode JSON, if desired, on build
*
* @returns The request builder, with add and build methods
*/
export function payloadJsonProcessor(encodeBase64: boolean): JsonProcessor {
return (
payloadBuilder: PayloadBuilder,
jsonForProcessing: EventJson,
contextEntitiesForProcessing: SelfDescribingJson[]
) => {
const add = (json: any, keyIfEncoded: string, keyIfNotEncoded: string): void => {
const str = JSON.stringify(json);
if (encodeBase64) {
payloadBuilder.add(keyIfEncoded, base64urlencode(str));
} else {
payloadBuilder.add(keyIfNotEncoded, str);
}
};
const getContextFromPayload = () => {
let payload = payloadBuilder.getPayload();
if (encodeBase64 ? payload.cx : payload.co) {
return JSON.parse(
encodeBase64 ? base64urldecode(payload.cx as string) : (payload.co as string)
) as SelfDescribingJsonArray;
}
return undefined;
};
const combineContexts = (
originalContext: SelfDescribingJsonArray | undefined,
newContext: SelfDescribingJsonArray
) => {
let context = originalContext || getContextFromPayload();
if (context) {
context.data = context.data.concat(newContext.data);
} else {
context = newContext;
}
return context;
};
let context: SelfDescribingJsonArray | undefined = undefined;
for (const json of jsonForProcessing) {
if (json.keyIfEncoded === 'cx') {
context = combineContexts(context, json.json as SelfDescribingJsonArray);
} else {
add(json.json, json.keyIfEncoded, json.keyIfNotEncoded);
}
}
jsonForProcessing.length = 0;
if (contextEntitiesForProcessing.length) {
let newContext = {
schema: 'iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0',
data: [...contextEntitiesForProcessing],
};
context = combineContexts(context, newContext);
contextEntitiesForProcessing.length = 0;
}
if (context) {
add(context, 'cx', 'co');
}
};
}
/**
* Is property a non-empty JSON?
* @param property - Checks if object is non-empty json
*/
export function isNonEmptyJson(property?: Record<string, unknown>): boolean {
if (!isJson(property)) {
return false;
}
for (const key in property) {
if (Object.prototype.hasOwnProperty.call(property, key)) {
return true;
}
}
return false;
}
/**
* Is property a JSON?
* @param property - Checks if object is json
*/
export function isJson(property?: Record<string, unknown>): boolean {
return (
typeof property !== 'undefined' &&
property !== null &&
(property.constructor === {}.constructor || property.constructor === [].constructor)
);
}