Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AM linker #194

Merged
merged 5 commits into from
Jan 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,38 @@ rudderanalytics.ready(
```

# [](https://github.com/rudderlabs/rudder-sdk-js/blob/master/README.md#adding-callbacks-to-standard-methods)Adding callbacks to standard methods
| **For detailed technical documentation and troubleshooting guide on the RudderStack’s JavaScript SDK, check out our [docs](https://docs.rudderlabs.com/sdk-integration-guide/getting-started-with-javascript-sdk).** |
|:------|

# [](https://github.com/rudderlabs/rudder-sdk-js/blob/master/README.md#querystring-api)Querystring API

RudderStack's Querystring API allows you to trigger `track`, `identify` calls using query parameters. If you pass the following parameters in the URL, then it will trigger the corresponding SDK API call.

For example:

```
http://hostname.com/?ajs_uid=12345&ajs_event=test%20event&ajs_aid=abcde&ajs_prop_testProp=prop1&ajs_trait_name=Firstname+Lastname
```
For the above URL, the below SDK calls will be triggered:

```
rudderanalytics.identify("12345", {name: "Firstname Lastname"});
rudderanalytics.track("test event", {testProp: "prop1"});
rudderanalytics.setAnonymousId("abcde");
```

You may use the below parameters as querystring parameter and trigger the corresponding call.

`ajs_uid` : Makes a `rudderanalytics.identify()` call with `userId` having the value of the parameter value.

`ajs_aid` : Makes a `rudderanalytics.setAnonymousId()` call with `anonymousId` having the value of the parameter value.

`ajs_event` : Makes a `rudderanalytics.track()` call with `event` name as parameter value.

`ajs_prop_<property>` : If `ajs_event` is passed as querystring, value of this parameter will populate the `properties` of the corresponding event in the `track` call.

`ajs_trait_<trait>` : If `ajs_uid` is provided as querysting, value of this parameter will populate the `traits` of the `identify` call made.


One can also define callbacks to common methods of `rudderanalytics` object. _**Note**_: For now, the functionality is supported for `syncPixel` method which is called in Rudder SDK when making sync calls in integrations for relevant destinations.

Expand Down
17 changes: 15 additions & 2 deletions analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { EventRepository } from "./utils/EventRepository";
import logger from "./utils/logUtil";
import { addDomEventHandlers } from "./utils/autotrack.js";
import ScriptLoader from "./integrations/ScriptLoader";
import parseLinker from "./utils/linker";

const queryDefaults = {
trait: "ajs_trait_",
Expand Down Expand Up @@ -830,9 +831,21 @@ class Analytics {
return this.anonymousId;
}

setAnonymousId(anonymousId) {
/**
* Sets anonymous id in the followin precedence:
* 1. anonymousId: Id directly provided to the function.
* 2. rudderAmpLinkerParm: value generated from linker query parm (rudderstack)
* using praseLinker util.
* 3. generateUUID: A new uniquie id is generated and assigned.
*
* @param {string} anonymousId
* @param {string} rudderAmpLinkerParm
*/
setAnonymousId(anonymousId, rudderAmpLinkerParm) {
// if (!this.loaded) return;
this.anonymousId = anonymousId || generateUUID();
const parsedAnonymousIdObj = rudderAmpLinkerParm ? parseLinker(rudderAmpLinkerParm) : null;
const parsedAnonymousId = parsedAnonymousIdObj ? parsedAnonymousIdObj.rs_amp_id : null;
this.anonymousId = anonymousId || parsedAnonymousId || generateUUID();
this.storage.setAnonymousId(this.anonymousId);
}

Expand Down
26 changes: 26 additions & 0 deletions utils/linker/base64decoder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @description This is utility function for decoding from base 64 to utf8
* @version v1.0.0
*/

/**
* @param {string} str base64
* @returns {string} utf8
*/
function b64DecodeUnicode(str) {
// Going backwards: from bytestream, to percent-encoding, to original string.
return decodeURIComponent(atob(str).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
}

/**
* @param {string} value
* @return {string}
*/
function decode(data = "") {
data = data.endsWith("..") ? data.substr(0, data.length - 2) : data;
return b64DecodeUnicode(data);
}

export default decode
42 changes: 42 additions & 0 deletions utils/linker/crc32.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* @description This is utility function for crc32 algorithm
* @version v1.0.0
*/

/**
* @description generate crc table
* @params none
* @returns arrray of CRC table
*/

const makeCRCTable = function () {
const crcTable = []
let c
for (var n = 0; n < 256; n++) {
c = n
for (var k = 0; k < 8; k++) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
}
crcTable[n] = c
}
return crcTable
}


/**
*
* @param {string} str
* @returns {Bystream} crc32
*/
const crc32 = function (str) {
const crcTable = makeCRCTable()
let crc = 0 ^ -1

for (let i = 0; i < str.length; i++) {
crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xff]
}

return (crc ^ -1) >>> 0
}

export default crc32
140 changes: 140 additions & 0 deletions utils/linker/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* @description AMP Linker Parser (works for Rudder, GA or any other linker created by following Google's linker standard.)
* @version v1.0.0
* @author Parth Mahajan, Ayush Mehra
*/

import crc32 from "./crc32";
import USER_INTERFACE from "./userLib";
import decode from "./base64decoder";

const KEY_VALIDATOR = /^[a-zA-Z0-9\-_.]+$/;
const CHECKSUM_OFFSET_MAX_MIN = 1;
const VALID_VERSION = 1;
const DELIMITER = "*";

/**
* Return the key value pairs
* @param {string} value
* @return {?Object<string, string>}
*/
function parseLinker(value) {
const linkerObj = parseLinkerParamValue(value);
if (!linkerObj) {
return null;
}
const { checksum, serializedIds } = linkerObj;
if (!isCheckSumValid(serializedIds, checksum)) {
return null;
}
return deserialize(serializedIds);
}

/**
* Parse the linker param value to version checksum and serializedParams
* @param {string} value
* @return {?Object}
*/
function parseLinkerParamValue(value) {
const parts = value.split(DELIMITER);
const isEven = parts.length % 2 == 0;

if (parts.length < 4 || !isEven) {
// Format <version>*<checksum>*<key1>*<value1>
// Note: linker makes sure there's at least one pair of non empty key value
// Make sure there is at least three delimiters.
return null;
}

const version = Number(parts.shift());
if (version !== VALID_VERSION) {
return null;
}

const checksum = parts.shift();
const serializedIds = parts.join(DELIMITER);
return {
checksum,
serializedIds,
};
}

/**
* Check if the checksum is valid with time offset tolerance.
* @param {string} serializedIds
* @param {string} checksum
* @return {boolean}
*/
function isCheckSumValid(serializedIds, checksum) {
const userAgent = USER_INTERFACE.getUserAgent();
const language = USER_INTERFACE.getUserLanguage();
for (let i = 0; i <= CHECKSUM_OFFSET_MAX_MIN; i++) {
const calculateCheckSum = getCheckSum(
serializedIds,
i,
userAgent,
language
);
if (calculateCheckSum == checksum) {
return true;
}
}
return false;
}

/**
* Deserialize the serializedIds and return keyValue pairs.
* @param {string} serializedIds
* @return {!Object<string, string>}
*/
function deserialize(serializedIds) {
const keyValuePairs = {};
const params = serializedIds.split(DELIMITER);
for (let i = 0; i < params.length; i += 2) {
const key = params[i];
const valid = KEY_VALIDATOR.test(key);
if (!valid) {
continue;
}
const value = decode(params[i + 1]);
//const value = params[i + 1];
keyValuePairs[key] = value;
}
return keyValuePairs;
}

/**
* Create a unique checksum hashing the fingerprint and a few other values.
* @param {string} serializedIds
* @param {number=} opt_offsetMin
* @return {string}
*/
function getCheckSum(serializedIds, opt_offsetMin, userAgent, language) {
const fingerprint = getFingerprint(userAgent, language);
const offset = opt_offsetMin || 0;
const timestamp = getMinSinceEpoch() - offset;
const crc = crc32([fingerprint, timestamp, serializedIds].join(DELIMITER));
// Encoded to base36 for less bytes.
return crc.toString(36);
}

/**
* Generates a semi-unique value for page visitor.
* @return {string}
*/
function getFingerprint(userAgent, language) {
const date = new Date();
const timezone = date.getTimezoneOffset();
return [userAgent, timezone, language].join(DELIMITER);
}

/**
* Rounded time used to check if t2 - t1 is within our time tolerance.
* @return {number}
*/
function getMinSinceEpoch() {
// Timestamp in minutes, floored.
return Math.floor(Date.now() / 60000);
}

export default parseLinker;
20 changes: 20 additions & 0 deletions utils/linker/userLib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @description An interface to fetch user device details.
* @version v1.0.0
*/

const USER_INTERFACE = {
/**
* @param {*} req
* @returns {string} user language
*/
getUserLanguage: () => navigator && navigator.language,

/**
* @param {*} req
* @returns {string} userAgent
*/
getUserAgent: () => navigator && navigator.userAgent
}

export default USER_INTERFACE;