Skip to content

Commit cfc7cc4

Browse files
author
Paul Boocock
committed
Prevent base64 encoder being bundled into each plugin (close #921)
1 parent 140c384 commit cfc7cc4

File tree

5 files changed

+125
-89
lines changed

5 files changed

+125
-89
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"comment": "Prevent base64 encoder being bundled into each plugin (#921)",
5+
"type": "none",
6+
"packageName": "@snowplow/tracker-core"
7+
}
8+
],
9+
"packageName": "@snowplow/tracker-core",
10+
"email": "[email protected]"
11+
}

libraries/tracker-core/src/core.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
*/
3030

3131
import { v4 as uuid } from 'uuid';
32-
import { payloadBuilder, PayloadBuilder, Payload, isJson } from './payload';
32+
import { payloadBuilder, PayloadBuilder, Payload, isJson, payloadJsonProcessor } from './payload';
3333
import {
3434
globalContexts,
3535
ConditionalContextProvider,
@@ -146,7 +146,7 @@ export interface TrackerCore {
146146
context?: Array<SelfDescribingJson> | null,
147147
/** Timestamp override */
148148
timestamp?: Timestamp | null
149-
) => PayloadBuilder;
149+
) => Payload;
150150

151151
/**
152152
* Set a persistent key-value pair to be added to every payload
@@ -363,8 +363,8 @@ export function trackerCore(configuration: CoreConfiguration = {}): TrackerCore
363363
pb: PayloadBuilder,
364364
context?: Array<SelfDescribingJson> | null,
365365
timestamp?: Timestamp | null
366-
): PayloadBuilder {
367-
pb.setBase64Encoding(encodeBase64);
366+
): Payload {
367+
pb.withJsonProcessor(payloadJsonProcessor(encodeBase64));
368368
pb.add('eid', uuid());
369369
pb.addDict(payloadPairs);
370370
const tstamp = getTimestamp(timestamp);
@@ -389,17 +389,19 @@ export function trackerCore(configuration: CoreConfiguration = {}): TrackerCore
389389
callback(pb);
390390
}
391391

392+
const finalPayload = pb.build();
393+
392394
corePlugins.forEach((plugin) => {
393395
try {
394396
if (plugin.afterTrack) {
395-
plugin.afterTrack(pb.build());
397+
plugin.afterTrack(finalPayload);
396398
}
397399
} catch (ex) {
398400
console.warn('Snowplow: error with plugin ', ex);
399401
}
400402
});
401403

402-
return pb;
404+
return finalPayload;
403405
}
404406

405407
/**

libraries/tracker-core/src/payload.ts

Lines changed: 74 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,20 @@ import { base64urlencode } from './base64';
3535
*/
3636
export type Payload = Record<string, unknown>;
3737

38+
/**
39+
* An array of tuples which represented the unprocessed JSON to be added to the Payload
40+
*/
41+
export type JsonForProcessing = Array<[keyIfEncoded: string, keyIfNotEncoded: string, json: Record<string, unknown>]>;
42+
43+
/**
44+
* A function which will processor the Json onto the injected PayloadBuilder
45+
*/
46+
export type JsonProcessor = (payloadBuilder: PayloadBuilder, jsonForProcessing: JsonForProcessing) => void;
47+
3848
/**
3949
* Interface for mutable object encapsulating tracker payload
4050
*/
4151
export interface PayloadBuilder {
42-
/**
43-
* Sets whether the JSON within the payload should be base64 encoded
44-
* @param base64 Toggle for base64 encoding
45-
*/
46-
setBase64Encoding: (base64: boolean) => void;
47-
4852
/**
4953
* Adds an entry to the Payload
5054
* @param key Key for Payload dictionary entry
@@ -66,55 +70,24 @@ export interface PayloadBuilder {
6670
*/
6771
addJson: (keyIfEncoded: string, keyIfNotEncoded: string, json: Record<string, unknown>) => void;
6872

73+
/**
74+
* Adds a function which will be executed when building
75+
* the payload to process the JSON which has been added to this payload
76+
* @param jsonProcessor The JsonProcessor function for this builder
77+
*/
78+
withJsonProcessor: (jsonProcessor: JsonProcessor) => void;
79+
6980
/**
7081
* Builds and returns the Payload
7182
* @param base64Encode configures if cached json should be encoded
7283
*/
7384
build: () => Payload;
7485
}
7586

76-
/**
77-
* Is property a non-empty JSON?
78-
* @param property Checks if object is non-empty json
79-
*/
80-
export function isNonEmptyJson(property?: Record<string, unknown>): boolean {
81-
if (!isJson(property)) {
82-
return false;
83-
}
84-
for (const key in property) {
85-
if (Object.prototype.hasOwnProperty.call(property, key)) {
86-
return true;
87-
}
88-
}
89-
return false;
90-
}
91-
92-
/**
93-
* Is property a JSON?
94-
* @param property Checks if object is json
95-
*/
96-
export function isJson(property?: Record<string, unknown>): boolean {
97-
return (
98-
typeof property !== 'undefined' &&
99-
property !== null &&
100-
(property.constructor === {}.constructor || property.constructor === [].constructor)
101-
);
102-
}
103-
104-
/**
105-
* A helper to build a Snowplow request from a set of name-value pairs, provided using the add methods.
106-
* Will base64 encode JSON, if desired, on build
107-
*
108-
* @return The request builder, with add and build methods
109-
*/
11087
export function payloadBuilder(): PayloadBuilder {
111-
const dict: Payload = {};
112-
const jsonForEncoding: Array<[string, string, Record<string, unknown>]> = [];
113-
let encodeBase64: boolean = true;
114-
115-
const setBase64Encoding = (base64: boolean) => {
116-
encodeBase64 = base64;
117-
};
88+
const dict: Payload = {},
89+
jsonForProcessing: JsonForProcessing = [];
90+
let processor: JsonProcessor | undefined;
11891

11992
const add = (key: string, value: unknown): void => {
12093
if (value != null && value !== '') {
@@ -133,29 +106,68 @@ export function payloadBuilder(): PayloadBuilder {
133106

134107
const addJson = (keyIfEncoded: string, keyIfNotEncoded: string, json?: Record<string, unknown>): void => {
135108
if (json && isNonEmptyJson(json)) {
136-
jsonForEncoding.push([keyIfEncoded, keyIfNotEncoded, json]);
109+
jsonForProcessing.push([keyIfEncoded, keyIfNotEncoded, json]);
137110
}
138111
};
139112

140-
const build = (): Payload => {
141-
for (const json of jsonForEncoding) {
113+
return {
114+
add,
115+
addDict,
116+
addJson,
117+
withJsonProcessor: (jsonProcessor) => {
118+
processor = jsonProcessor;
119+
},
120+
build: function () {
121+
processor?.(this, jsonForProcessing);
122+
return dict;
123+
},
124+
};
125+
}
126+
127+
/**
128+
* A helper to build a Snowplow request from a set of name-value pairs, provided using the add methods.
129+
* Will base64 encode JSON, if desired, on build
130+
*
131+
* @return The request builder, with add and build methods
132+
*/
133+
export function payloadJsonProcessor(encodeBase64: boolean): JsonProcessor {
134+
return (payloadBuilder: PayloadBuilder, jsonForProcessing: JsonForProcessing) => {
135+
for (const json of jsonForProcessing) {
142136
const str = JSON.stringify(json[2]);
143137
if (encodeBase64) {
144-
add(json[0], base64urlencode(str));
138+
payloadBuilder.add(json[0], base64urlencode(str));
145139
} else {
146-
add(json[1], str);
140+
payloadBuilder.add(json[1], str);
147141
}
148142
}
149-
jsonForEncoding.length = 0;
150-
151-
return dict;
143+
jsonForProcessing.length = 0;
152144
};
145+
}
153146

154-
return {
155-
setBase64Encoding,
156-
add,
157-
addDict,
158-
addJson,
159-
build,
160-
};
147+
/**
148+
* Is property a non-empty JSON?
149+
* @param property Checks if object is non-empty json
150+
*/
151+
export function isNonEmptyJson(property?: Record<string, unknown>): boolean {
152+
if (!isJson(property)) {
153+
return false;
154+
}
155+
for (const key in property) {
156+
if (Object.prototype.hasOwnProperty.call(property, key)) {
157+
return true;
158+
}
159+
}
160+
return false;
161+
}
162+
163+
/**
164+
* Is property a JSON?
165+
* @param property Checks if object is json
166+
*/
167+
export function isJson(property?: Record<string, unknown>): boolean {
168+
return (
169+
typeof property !== 'undefined' &&
170+
property !== null &&
171+
(property.constructor === {}.constructor || property.constructor === [].constructor)
172+
);
161173
}

libraries/tracker-core/test/core.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import {
5151
buildStructEvent,
5252
trackerCore,
5353
} from '../src/core';
54-
import { PayloadBuilder, Payload } from '../src/payload';
54+
import { Payload } from '../src/payload';
5555

5656
const selfDescribingEventSchema = 'iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0';
5757
let beforeCount = 0,
@@ -60,13 +60,12 @@ const tracker = trackerCore({
6060
base64: false,
6161
corePlugins: [{ beforeTrack: () => (beforeCount += 1), afterTrack: () => (afterCount += 1) }],
6262
});
63-
function compare(result: PayloadBuilder, expected: Payload, t: ExecutionContext) {
64-
const res = result.build();
65-
t.truthy(res['eid'], 'A UUID should be attached to all events');
66-
delete res['eid'];
67-
t.truthy(res['dtm'], 'A timestamp should be attached to all events');
68-
delete res['dtm'];
69-
t.deepEqual(res, expected);
63+
function compare(result: Payload, expected: Payload, t: ExecutionContext) {
64+
t.truthy(result['eid'], 'A UUID should be attached to all events');
65+
delete result['eid'];
66+
t.truthy(result['dtm'], 'A timestamp should be attached to all events');
67+
delete result['dtm'];
68+
t.deepEqual(result, expected);
7069
}
7170

7271
test('should track a page view', (t) => {
@@ -670,9 +669,11 @@ test('should track a page view with custom context', (t) => {
670669
test('should track a page view with a timestamp', (t) => {
671670
const timestamp = 1000000000000;
672671
t.is(
673-
tracker
674-
.track(buildPageView({ pageUrl: 'http://www.example.com', pageTitle: 'title', referrer: 'ref' }), [], timestamp)
675-
.build()['dtm'],
672+
tracker.track(
673+
buildPageView({ pageUrl: 'http://www.example.com', pageTitle: 'title', referrer: 'ref' }),
674+
[],
675+
timestamp
676+
)['dtm'],
676677
'1000000000000'
677678
);
678679
});
@@ -736,7 +737,7 @@ test('should execute a callback', (t) => {
736737
corePlugins: [],
737738
callback: function (payload) {
738739
const callbackTarget = payload;
739-
compare(callbackTarget, expected, t);
740+
compare(callbackTarget.build(), expected, t);
740741
},
741742
});
742743
const pageUrl = 'http://www.example.com';
@@ -790,9 +791,10 @@ test('should set true timestamp', (t) => {
790791
const pageUrl = 'http://www.example.com';
791792
const pageTitle = 'title page';
792793
const referrer = 'https://www.google.com';
793-
const result = tracker
794-
.track(buildPageView({ pageUrl, pageTitle, referrer }), undefined, { type: 'ttm', value: 1477403862 })
795-
.build();
794+
const result = tracker.track(buildPageView({ pageUrl, pageTitle, referrer }), undefined, {
795+
type: 'ttm',
796+
value: 1477403862,
797+
});
796798
t.true('ttm' in result);
797799
t.is(result['ttm'], '1477403862');
798800
t.false('dtm' in result);
@@ -805,9 +807,10 @@ test('should set device timestamp as ADT', (t) => {
805807
name: 'Eric',
806808
},
807809
};
808-
const result = tracker
809-
.track(buildSelfDescribingEvent({ event: inputJson }), [inputJson], { type: 'dtm', value: 1477403869 })
810-
.build();
810+
const result = tracker.track(buildSelfDescribingEvent({ event: inputJson }), [inputJson], {
811+
type: 'dtm',
812+
value: 1477403869,
813+
});
811814
t.true('dtm' in result);
812815
t.is(result['dtm'], '1477403869');
813816
t.false('ttm' in result);

libraries/tracker-core/test/payload.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
*/
3030

3131
import test from 'ava';
32-
import { isJson, isNonEmptyJson, payloadBuilder } from '../src/payload';
32+
import { isJson, isNonEmptyJson, payloadBuilder, payloadJsonProcessor } from '../src/payload';
3333

3434
const sampleJson = {
3535
schema: 'iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0',
@@ -124,15 +124,23 @@ test('Add a dictionary of name-value pairs to the payload', (t) => {
124124

125125
test('Add a JSON to the payload', (t) => {
126126
const sb = payloadBuilder();
127-
sb.setBase64Encoding(false);
127+
sb.withJsonProcessor(payloadJsonProcessor(false));
128128
sb.addJson('cx', 'co', sampleJson);
129129

130130
t.deepEqual(sb.build(), expectedPayloads[0], 'JSON should be added correctly');
131131
});
132132

133133
test('Add a base 64 encoded JSON to the payload', (t) => {
134-
const sb = payloadBuilder(); // base64 encoding on by default
134+
const sb = payloadBuilder();
135+
sb.withJsonProcessor(payloadJsonProcessor(true));
135136
sb.addJson('cx', 'co', sampleJson);
136137

137138
t.deepEqual(sb.build(), expectedPayloads[1], 'JSON should be encoded correctly');
138139
});
140+
141+
test('payloadBuilder with no json processor, processes no json', (t) => {
142+
const sb = payloadBuilder();
143+
sb.addJson('cx', 'co', sampleJson);
144+
145+
t.deepEqual(sb.build(), {}, 'JSON should be missing');
146+
});

0 commit comments

Comments
 (0)