Skip to content
Open
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
158 changes: 86 additions & 72 deletions analytics.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,65 @@
import { Platform, Dimensions } from 'react-native';
import { Constants } from 'expo';
import { Platform, Dimensions } from "react-native";
import { Constants } from "expo";

import { ScreenHit, PageHit, Event, Serializable } from './hits';
import { ScreenHit, PageHit, Event, Serializable } from "./hits";
import HitValidator from "./validation";

const { width, height } = Dimensions.get('window');
const { width, height } = Dimensions.get("window");

let defaultOptions = { debug: false };

export default class Analytics {
customDimensions = []

constructor(propertyId, additionalParameters = {}, options = defaultOptions){
this.propertyId = propertyId;
this.options = options;
this.clientId = Constants.deviceId;

this.promiseGetWebViewUserAgentAsync = Constants.getWebViewUserAgentAsync()
.then(userAgent => {
this.userAgent = userAgent;

this.parameters = {
an: Constants.manifest.name,
aid: Constants.manifest.slug,
av: Constants.manifest.version,
sr: `${width}x${height}`,
...additionalParameters
};

if(this.options.debug){
console.log(`[expo-analytics] UserAgent=${userAgent}`);
console.log(`[expo-analytics] Additional parameters=`, this.parameters);
}
});
}

hit(hit){
// send only after the user agent is saved
return this.promiseGetWebViewUserAgentAsync
.then(() => this.send(hit));
}

event(event){
// send only after the user agent is saved
return this.promiseGetWebViewUserAgentAsync
.then(() => this.send(event));
}

addCustomDimension(index, value){
this.customDimensions[index] = value;
}

removeCustomDimension(index){
delete this.customDimensions[index];
}

send(hit) {
/* format: https://www.google-analytics.com/collect? +
customDimensions = [];

constructor(propertyId, additionalParameters = {}, options = defaultOptions) {
this.propertyId = propertyId;
this.options = options;
this.clientId = Constants.deviceId;
this.hitValidator = new HitValidator({ debug: this.options.debug });

this.promiseGetWebViewUserAgentAsync = Constants.getWebViewUserAgentAsync().then(
userAgent => {
this.userAgent = userAgent;

this.parameters = {
an: Constants.manifest.name,
aid: Constants.manifest.slug,
av: Constants.manifest.version,
sr: `${width}x${height}`,
...additionalParameters
};

if (this.options.debug) {
console.log(`[expo-analytics] UserAgent=${userAgent}`);
console.log(
`[expo-analytics] Additional parameters=`,
this.parameters
);
}
}
);
}

hit(hit) {
// send only after the user agent is saved
return this.promiseGetWebViewUserAgentAsync.then(() => this.send(hit));
}

event(event) {
// send only after the user agent is saved
return this.promiseGetWebViewUserAgentAsync.then(() => this.send(event));
}

addCustomDimension(index, value) {
this.customDimensions[index] = value;
}

removeCustomDimension(index) {
delete this.customDimensions[index];
}

send(hit) {
/* format: https://www.google-analytics.com/collect? +
* &tid= GA property ID (required)
* &v= GA protocol version (always 1) (required)
* &t= hit type (pageview / screenview)
Expand All @@ -72,24 +76,34 @@ export default class Analytics {
* &z= cache buster (prevent browsers from caching GET requests -- should always be last)
*/

const customDimensions = this.customDimensions.map((value, index) => `cd${index}=${value}`).join('&');

const params = new Serializable(this.parameters).toQueryString();

const url = `https://www.google-analytics.com/collect?tid=${this.propertyId}&v=1&cid=${this.clientId}&${hit.toQueryString()}&${params}&${customDimensions}&z=${Math.round(Math.random() * 1e8)}`;

let options = {
method: 'get',
headers: {
'User-Agent': this.userAgent
}
}

if(this.options.debug){
console.log(`[expo-analytics] Sending GET request to ${url}`);
}

return fetch(url, options);
}

const customDimensions = this.customDimensions
.map((value, index) => `cd${index}=${value}`)
.join("&");

const params = new Serializable(this.parameters).toQueryString();

const url = `https://www.google-analytics.com/collect?tid=${
this.propertyId
}&v=1&cid=${
this.clientId
}&${hit.toQueryString()}&${params}&${customDimensions}&z=${Math.round(
Math.random() * 1e8
)}`;

let options = {
method: "get",
headers: {
"User-Agent": this.userAgent
}
};

return this.hitValidator.validate({ url }).then(valid => {
if (valid) {
if (this.options.debug) {
console.log(`[expo-analytics] Sending GET request to ${url}`);
}
return fetch(url, options);
}
});
}
}
61 changes: 61 additions & 0 deletions validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export default class HitValidator {
constructor({ debug }) {
this.debug = debug;
}

validate = ({ url }) => {
// Do not validate in prod mode
if (!this.debug) return Promise.resolve(true);

const hostname = this.extractHostname(url);
const queryStr = url.slice(url.indexOf(hostname) + hostname.length);

return fetch(`https://${hostname}/debug${queryStr}`, { method: "get" })
.then(result => result.json())
.then(res => {
if (res.hitParsingResult && Array.isArray(res.hitParsingResult)) {
const validationResult = res.hitParsingResult[0];
const { valid, parserMessage } = validationResult;
if (valid) return true;

// INFO message will not prevent hit from being successfully sent
// but WARN will...which is confusing for me...I thought only ERROR
// will cause hit to be not recorded
parserMessage
.filter(m => m.messageType !== "INFO")
.forEach(m => this.outputError(m, url));
return false;
}
throw new Error(`Unexpected hitParsingResult: ${res.hitParsingResult}`);
})
.catch(err => {
console.log(
`[expo-analytics] Failed to validate hit request: ${err.message}`
);
});
};

outputError = (message, url) => {
const { description, messageType } = message;
console.log(
`[expo-analytics] Failed to pass validation for ${url}:\n ${messageType} ${description}`
);
};

extractHostname = url => {
var hostname;
//find & remove protocol (http, ftp, etc.) and get hostname
if (url.indexOf("//") > -1) {
hostname = url.split("/")[2];
} else {
hostname = url.split("/")[0];
}

//find & remove port number
hostname = hostname.split(":")[0];
//find & remove "?"
hostname = hostname.split("?")[0];

return hostname;
};
}