@snowplow/javascript-tracker
Version:
Web analytics for Snowplow
811 lines (757 loc) • 24.7 kB
text/typescript
/*
* 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 util from 'util';
import F from 'lodash/fp';
import { DockerWrapper, fetchResults, start, stop } from '../micro';
import { version } from '../../package.json';
const dumpLog = (log: Array<unknown>) => console.log(util.inspect(log, true, null, true));
const retrieveSchemaData = (schema: unknown) => F.compose(F.get('data'), F.find({ schema }), F.get('data'));
const loadUrlAndWait = async (url: string) => {
await browser.url(url);
await browser.waitUntil(async () => (await $('#init').getText()) === 'true', {
timeout: 5000,
timeoutMsg: 'expected init after 5s',
});
await browser.waitUntil(async () => (await $('#secondInit').getText()) === 'true', {
timeout: 5000,
timeoutMsg: 'expected init after 5s',
});
};
const isMatchWithCallback = F.isMatchWith((lt, rt) => (F.isFunction(rt) ? rt(lt) : undefined));
const mobileContext = {
schema: 'iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-1',
data: {
osType: 'ubuntu',
},
};
const geoContext = {
schema: 'iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0',
data: {
latitude: 40.0,
longitude: 55.1,
},
};
describe('Snowplow Micro integration', () => {
let eventMethods = ['get', 'post', 'beacon'];
let log: Array<unknown> = [];
let docker: DockerWrapper;
const logContains = (ev: unknown) => F.some(F.isMatch(ev as object), log);
const logContainsFn = (ev: unknown) => F.some(isMatchWithCallback(ev as object), log);
beforeAll(async () => {
await browser.call(async () => (docker = await start()));
await browser.url('/index.html');
await browser.setCookies({ name: 'container', value: docker.url });
await loadUrlAndWait('/integration.html?eventMethod=get');
await $('#bottomRight').scrollIntoView();
await browser.pause(10000); // Time for requests to get written
await loadUrlAndWait('/integration.html?eventMethod=post');
await $('#bottomRight').scrollIntoView();
await browser.pause(6000); // Time for requests to get written
await loadUrlAndWait('/integration.html?eventMethod=beacon');
await $('#bottomRight').scrollIntoView();
await browser.pause(6000); // Time for requests to get written
log = await browser.call(async () => await fetchResults(docker.url));
});
afterAll(async () => {
await browser.call(async () => await stop(docker.container));
});
eventMethods.forEach((method) => {
it(`${method}: contains correct tracker version`, () => {
expect(
logContains({
rawEvent: {
parameters: {
tv: `js-${version}`,
},
},
event: {
app_id: `sp-${method}`,
v_tracker: `js-${version}`,
},
})
).toBe(true);
});
it(`${method}: contains page view`, () => {
expect(
logContains({
event: {
event: 'page_view',
platform: 'mob',
app_id: `sp-${method}`,
user_id: 'Malcolm',
page_title: 'Integration test page',
},
})
).toBe(true);
});
it(`${method}: contains page view with custom context in log`, () => {
const expected = {
event: {
event: 'page_view',
platform: 'mob',
app_id: `sp-${method}`,
user_id: 'Malcolm',
page_title: 'My Title',
contexts: {
data: [
{
schema: 'iglu:org.schema/WebPage/jsonschema/1-0-0',
data: { keywords: ['tester'] },
},
],
},
},
};
expect(logContains(expected)).toBe(true);
});
it(`${method}: contains nonnonexistent event types in log`, () => {
expect(
logContains({
event: 'ad',
app_id: `sp-${method}`,
})
).toBe(false);
});
it(`${method}: contains at least one ping in the expected interval`, () => {
const gtZero = F.compose(F.negate(F.gt(0)), F.toNumber);
expect(
logContainsFn({
event: {
event_name: 'page_ping',
app_id: `sp-${method}`,
pp_xoffset_max: gtZero,
pp_yoffset_max: gtZero,
},
})
).toBe(true);
});
it(`${method}: contains a structured event`, () => {
expect(
logContains({
event: {
app_id: `sp-${method}`,
event: 'struct',
se_category: 'Mixes',
se_action: 'Play',
se_label: 'MRC/fabric-0503-mix',
se_value: 0.0,
},
})
).toBe(true);
});
it(`${method}: contains an self describing event with true timestamp`, () => {
expect(
logContains({
event: {
app_id: `sp-${method}`,
event: 'unstruct',
unstruct_event: {
data: {
data: { bannerId: 'ASO01043' },
},
},
true_tstamp: '1970-01-18T02:23:21.868Z',
},
})
).toBe(true);
});
it(`${method}: contains a transaction event`, () => {
expect(
logContains({
event: {
app_id: `sp-${method}`,
event: 'transaction',
tr_orderid: 'order-123',
tr_affiliation: 'acme',
tr_total: 8000,
tr_tax: 100,
tr_shipping: 50,
tr_city: 'pheonix',
tr_state: 'arizona',
tr_country: 'USA',
tr_currency: 'JPY',
},
})
).toBe(true);
});
it(`${method}: contains a transaction item event`, () => {
expect(
logContains({
event: {
app_id: `sp-${method}`,
event: 'transaction_item',
ti_orderid: 'order-123',
ti_sku: '1001',
ti_name: 'Blue t-shirt',
ti_category: 'clothing',
ti_price: 2000,
ti_quantity: 2,
ti_currency: 'JPY',
},
})
).toBe(true);
});
it(`${method}: contains an unhandled exception event`, () => {
expect(
logContains({
event: {
app_id: `sp-${method}`,
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/application_error/jsonschema/1-0-1',
},
},
contexts: {
data: [
{
schema: 'iglu:org.schema/WebPage/jsonschema/1-0-0',
data: { keywords: ['tester'] },
},
],
},
},
})
).toBe(true);
});
it(`${method}: shows that pageViewId is regenerated for each trackPageView`, () => {
const pageViews = F.filter(
(ev) => F.get('event.event', ev) === 'page_view' && F.get('event.name_tracker', ev) === 'sp',
log
);
const getWebPageId = F.compose(
F.get('id'),
retrieveSchemaData('iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0'),
F.get('event.contexts')
);
expect(F.size(F.groupBy(getWebPageId, pageViews))).toBeGreaterThanOrEqual(2);
});
it(`${method}: contains a GDPR context`, () => {
expect(
logContains({
event: {
app_id: `sp-${method}`,
contexts: {
data: [
{
schema: 'iglu:com.snowplowanalytics.snowplow/gdpr/jsonschema/1-0-0',
data: {
basisForProcessing: 'consent',
documentId: 'someId',
documentVersion: '0.1.0',
documentDescription: 'this document is a test',
},
},
],
},
},
})
).toBe(true);
});
it(`${method}: has a GDPR context attached to all events`, () => {
const withoutGdprContext = F.compose(
F.negate(F.includes('iglu:com.snowplowanalytics.snowplow/gdpr/jsonschema/1-0-0')),
F.get('contexts')
);
const fromCfTracker = F.compose(F.eq('cf'), F.get('event.name_tracker'));
const numberWithoutGdpr = F.size(F.filter((ev) => withoutGdprContext(ev) && fromCfTracker(ev), log));
expect(numberWithoutGdpr).toBe(0);
});
it(`${method}: has global contexts attached to structured events`, () => {
expect(
logContains({
event: {
app_id: `sp-${method}`,
event: 'struct',
contexts: {
data: [geoContext, mobileContext],
},
},
})
).toBe(true);
});
it(`${method}: contains an self describing event with global context from accept ruleset`, () => {
expect(
logContains({
event: {
app_id: `sp-${method}`,
event: 'unstruct',
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/ad_impression/jsonschema/1-0-0',
data: {
bannerId: 'ASO01042',
},
},
},
contexts: {
data: [geoContext, mobileContext],
},
},
})
).toBe(true);
});
it(`${method}: contains an self describing event missing global context from reject ruleset`, () => {
expect(
logContains({
event: {
app_id: `sp-${method}`,
event: 'unstruct',
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/ad_impression/jsonschema/1-0-0',
data: {
bannerId: 'ASO01041',
},
},
},
contexts: {
data: [],
},
},
})
).toBe(true);
});
it(`${method}: contains page view event with non-base64 encoded context payload`, () => {
expect(
logContains({
rawEvent: {
parameters: {
co: '{"schema":"iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0","data":[{"schema":"iglu:org.schema/WebPage/jsonschema/1-0-0","data":{"keywords":["tester"]}}]}',
},
},
event: {
event: 'page_view',
platform: 'web',
app_id: `no-b64-${method}`,
page_title: 'My Title',
contexts: {
data: [
{
schema: 'iglu:org.schema/WebPage/jsonschema/1-0-0',
data: { keywords: ['tester'] },
},
],
},
},
})
).toBe(true);
});
it(`${method}: contains social interaction event`, () => {
expect(
logContains({
event: {
event: 'unstruct',
platform: 'mob',
app_id: `sp-${method}`,
user_id: 'Malcolm',
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/social_interaction/jsonschema/1-0-0',
data: { action: 'retweet', network: 'twitter', target: '1234' },
},
},
},
})
).toBe(true);
});
it(`${method}: contains ad impression event`, () => {
expect(
logContains({
event: {
event: 'unstruct',
platform: 'mob',
app_id: `sp-${method}`,
user_id: 'Malcolm',
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/ad_impression/jsonschema/1-0-0',
data: {
impressionId: '67965967893',
costModel: 'cpm',
cost: 5.5,
targetUrl: 'http://www.example.com',
bannerId: '23',
zoneId: '7',
advertiserId: '201',
campaignId: '12',
},
},
},
},
})
).toBe(true);
});
it(`${method}: contains ad click event`, () => {
expect(
logContains({
event: {
event: 'unstruct',
platform: 'mob',
app_id: `sp-${method}`,
user_id: 'Malcolm',
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/ad_click/jsonschema/1-0-0',
data: {
impressionId: '67965967893',
clickId: '12243253',
costModel: 'cpm',
cost: 2.5,
targetUrl: 'http://www.example.com',
bannerId: '23',
zoneId: '7',
advertiserId: '201',
campaignId: '12',
},
},
},
},
})
).toBe(true);
});
it(`${method}: contains ad conversion event`, () => {
expect(
logContains({
event: {
event: 'unstruct',
platform: 'mob',
app_id: `sp-${method}`,
user_id: 'Malcolm',
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/ad_conversion/jsonschema/1-0-0',
data: {
conversionId: '743560297',
costModel: 'cpa',
cost: 10,
category: 'ecommerce',
action: 'purchase',
property: '',
initialValue: 99,
advertiserId: '201',
campaignId: '12',
},
},
},
},
})
).toBe(true);
});
it(`${method}: contains add to cart event`, () => {
expect(
logContains({
event: {
event: 'unstruct',
platform: 'mob',
app_id: `sp-${method}`,
user_id: 'Malcolm',
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/add_to_cart/jsonschema/1-0-0',
data: {
sku: '000345',
name: 'blue tie',
category: 'clothing',
unitPrice: 3.49,
quantity: 2,
currency: 'GBP',
},
},
},
},
})
).toBe(true);
});
it(`${method}: contains remove from cart event`, () => {
expect(
logContains({
event: {
event: 'unstruct',
platform: 'mob',
app_id: `sp-${method}`,
user_id: 'Malcolm',
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/remove_from_cart/jsonschema/1-0-0',
data: {
sku: '000345',
name: 'blue tie',
category: 'clothing',
unitPrice: 3.49,
quantity: 1,
currency: 'GBP',
},
},
},
},
})
).toBe(true);
});
it(`${method}: contains site search event`, () => {
expect(
logContains({
event: {
event: 'unstruct',
platform: 'mob',
app_id: `sp-${method}`,
user_id: 'Malcolm',
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/site_search/jsonschema/1-0-0',
data: {
terms: ['unified', 'log'],
filters: { category: 'books', 'sub-category': 'non-fiction' },
totalResults: 14,
pageResults: 8,
},
},
},
},
})
).toBe(true);
});
it(`${method}: contains timing event`, () => {
expect(
logContains({
event: {
event: 'unstruct',
platform: 'mob',
app_id: `sp-${method}`,
user_id: 'Malcolm',
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0',
data: {
category: 'load',
variable: 'map_loaded',
timing: 50,
label: 'Map loading time',
},
},
},
},
})
).toBe(true);
});
it(`${method}: contains consent granted event`, () => {
expect(
logContains({
event: {
event: 'unstruct',
platform: 'mob',
app_id: `sp-${method}`,
user_id: 'Malcolm',
contexts: {
data: [
{
schema: 'iglu:com.snowplowanalytics.snowplow/consent_document/jsonschema/1-0-0',
data: {
id: '1234',
version: '5',
name: 'consent_document',
description: 'a document granting consent',
},
},
],
},
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/consent_granted/jsonschema/1-0-0',
data: {
expiry: '2020-11-21T08:00:00.000Z',
},
},
},
},
})
).toBe(true);
});
it(`${method}: contains consent withdrawn event`, () => {
expect(
logContains({
event: {
event: 'unstruct',
platform: 'mob',
app_id: `sp-${method}`,
user_id: 'Malcolm',
contexts: {
data: [
{
schema: 'iglu:com.snowplowanalytics.snowplow/consent_document/jsonschema/1-0-0',
data: {
id: '1234',
version: '5',
name: 'consent_document',
description: 'a document withdrawing consent',
},
},
],
},
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/consent_withdrawn/jsonschema/1-0-0',
data: {
all: false,
},
},
},
},
})
).toBe(true);
});
it(`${method}: contains custom error event`, () => {
expect(
logContains({
event: {
event: 'unstruct',
platform: 'mob',
app_id: `sp-${method}`,
user_id: 'Malcolm',
unstruct_event: {
data: {
schema: 'iglu:com.snowplowanalytics.snowplow/application_error/jsonschema/1-0-1',
data: {
programmingLanguage: 'JAVASCRIPT',
lineNumber: 237,
lineColumn: 5,
fileName: 'trackError.js',
},
},
},
},
})
).toBe(true);
});
it(`${method}: contains enhanced ecommerce event`, () => {
expect(
logContains({
event: {
event: 'unstruct',
platform: 'mob',
app_id: `sp-${method}`,
user_id: 'Malcolm',
contexts: {
data: [
{
schema: 'iglu:com.google.analytics.enhanced-ecommerce/actionFieldObject/jsonschema/1-0-0',
data: {
id: 'T12345',
affiliation: 'Google Store - Online',
revenue: 37.39,
tax: 2.85,
shipping: 5.34,
coupon: 'WINTER2016',
},
},
{
schema: 'iglu:com.google.analytics.enhanced-ecommerce/impressionFieldObject/jsonschema/1-0-0',
data: {
id: 'P12345',
name: 'Android Warhol T-Shirt',
list: 'Search Results',
brand: 'Google',
category: 'Apparel/T-Shirts',
variant: 'Black',
position: 1,
},
},
{
schema: 'iglu:com.google.analytics.enhanced-ecommerce/productFieldObject/jsonschema/1-0-0',
data: {
id: 'P12345',
name: 'Android Warhol T-Shirt',
list: 'Search Results',
brand: 'Google',
category: 'Apparel/T-Shirts',
variant: 'Black',
quantity: 1,
},
},
{
schema: 'iglu:com.google.analytics.enhanced-ecommerce/promoFieldObject/jsonschema/1-0-0',
data: {
id: 'PROMO_1234',
name: 'Summer Sale',
creative: 'summer_banner2',
position: 'banner_slot1',
},
},
],
},
unstruct_event: {
data: {
schema: 'iglu:com.google.analytics.enhanced-ecommerce/action/jsonschema/1-0-0',
data: {
action: 'purchase',
},
},
},
},
})
).toBe(true);
});
it(`${method}: doesn't contain a structured event with a user id (unset with null)`, () => {
expect(
logContains({
event: {
app_id: `sp-${method}`,
event: 'struct',
se_category: 'userIdTest1',
user_id: 'Dave',
},
})
).toBe(false);
});
it(`${method}: doesn't contain a structured event with a user id (unset with undefined)`, () => {
expect(
logContains({
event: {
app_id: `sp-${method}`,
event: 'struct',
se_category: 'userIdTest2',
user_id: 'Dave',
},
})
).toBe(false);
});
it(`${method}: has custom headers attached if possible (not with Beacon or IE9)`, () => {
const results = log.filter(
(event: any) =>
event.rawEvent.context.headers.includes('Content-Language: de-DE, en-CA') &&
event.event.app_id === `sp-${method}`
) as Array<any>;
if (method === 'beacon' || F.isMatch({ browserName: 'internet explorer', version: '9' }, browser.capabilities)) {
expect(results.length).toBe(0);
} else {
expect(results.length).toBeGreaterThanOrEqual(1);
}
});
});
});