├── .editorconfig ├── .gitignore ├── LICENSE.txt ├── README.md ├── example ├── README.md ├── app.js ├── bin │ └── www ├── package.json ├── public │ ├── javascripts │ │ └── purser.js │ └── stylesheets │ │ └── style.css ├── routes │ └── index.js └── views │ ├── error.jade │ ├── index.jade │ └── layout.jade ├── index.html ├── package.json ├── purser.js └── purser.min.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | npm-debug.log 4 | .sass-cache 5 | node_modules 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Bill Franklin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Purser 2 | 3 | A lightweight JavaScript library for preserving user data from first website visit to signup. 4 | 5 | ## Usage 6 | 7 | The library stores data such as `utm_medium`, `landing_page` and `referrer` in a localStorage object, and makes the object available via a handy API with the following methods: 8 | 9 | ``` 10 | purser.create() // automatically called on first website visit 11 | purser.fetch() // returns the object 12 | purser.convert(obj) // returns the object updated with conversion data 13 | purser.update(obj) // lets you add additional parameters to the object 14 | purser.destroy() // removes the object from localStorage 15 | 16 | ~~ NEW in version 1.1.0: track visits and pageviews ~~ 17 | purser.visits.create() // adds a visit instance to the object, called automatically if has been over 30 minutes since the most recent visit. 18 | purser.visits.all() // returns an array of all visits 19 | purser.visits.fetch(id) // Fetch a specified visit 20 | purser.visits.update(id, obj) // Update a specified visit with data in an object 21 | purser.visits.delete(id) // Delete a specified visit 22 | ``` 23 | 24 | View a live example at [http://purser.herokuapp.com/](http://purser.herokuapp.com/?utm_medium=github&utm_source=github_repo_example). 25 | 26 | ## Installation 27 | 28 | ``` 29 | git clone https://github.com/bilbof/purser 30 | ``` 31 | 32 | 1. Add [purser.min.js](https://github.com/bilbof/purser/blob/master/purser.min.js) to every page on your website. When a visitor creates an account, call purser.convert(obj) and add the user to your CRM/ChartMogul with attributes returned. 33 | 2. When a visitor creates an account, call purser.convert(obj) to get the visitor's marketing attributes 34 | 3. Add the user's marketing attributes to them in your CRM or app 35 | 36 | ## Example 37 | 38 | See an example at [http://github.com/bilbof/purser/example](https://github.com/bilbof/purser/tree/master/example). 39 | 40 | Code example 41 | 42 | ```html 43 | 44 | 54 | ``` 55 | 56 | The `attributes` object in the example above would look something like this: 57 | 58 | ```js 59 | { 60 | "first_website_visit": "2017-08-10T17:52:18.088Z", 61 | "referrer": "www.google.co.uk", 62 | "browser_timezone": 0, 63 | "browser_language": "en-GB", 64 | "landing_page": "http://localhost:5000/product", 65 | "screen_height": 800, 66 | "screen_width": 1280, 67 | "utm_medium": "google_search_ads", 68 | "utm_source": "google", 69 | "sign_up_button": "green-button", 70 | "converted_at": "2017-08-10T17:52:41.981Z", 71 | "visits_at_conversion": 12, 72 | "pageviews": 17, 73 | "last_visit": "2017-08-10T17:45:00", 74 | "visits": [], 75 | "conversion_page": "http://localhost:5000/signup" 76 | } 77 | ``` 78 | 79 | ## Contributing 80 | 81 | If you would like to contribute, PLEASE DO! Just create a [Pull Request](https://github.com/bilbof/purser/compare) once you've made your changes. 82 | 83 | Want a feature added, but would prefer not to do it yourself? [Create an Issue](https://github.com/bilbof/purser/issues/new). 84 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Purser Example 2 | 3 | This Node.js app demonstrates how to use Purser on a website or application. 4 | 5 | ## Installation 6 | 7 | ``` 8 | git clone https://github.com/bilbof/purser 9 | cd purser/example 10 | npm install 11 | npm start 12 | ``` 13 | 14 | The app will be available at [localhost:3000](http://localhost:3000). 15 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var logger = require('morgan'); 4 | var cookieParser = require('cookie-parser'); 5 | var bodyParser = require('body-parser'); 6 | 7 | var routes = require('./routes/index'); 8 | 9 | var app = express(); 10 | 11 | // view engine setup 12 | app.set('views', path.join(__dirname, 'views')); 13 | app.set('view engine', 'jade'); 14 | 15 | app.use(bodyParser.json()); 16 | app.use(bodyParser.urlencoded({ extended: false })); 17 | app.use(cookieParser()); 18 | app.use(express.static(path.join(__dirname, 'public'))); 19 | app.use('/', routes); 20 | 21 | // catch 404 and forward to error handler 22 | app.use(function(req, res, next) { 23 | var err = new Error('Not Found'); 24 | err.status = 404; 25 | next(err); 26 | }); 27 | 28 | // error handlers 29 | 30 | // development error handler 31 | // will print stacktrace 32 | if (app.get('env') === 'development') { 33 | app.use(function(err, req, res, next) { 34 | res.status(err.status || 500); 35 | res.render('error', { 36 | message: err.message, 37 | error: err 38 | }); 39 | }); 40 | } 41 | 42 | // production error handler 43 | // no stacktraces leaked to user 44 | app.use(function(err, req, res, next) { 45 | res.status(err.status || 500); 46 | res.render('error', { 47 | message: err.message, 48 | error: {} 49 | }); 50 | }); 51 | 52 | 53 | module.exports = app; 54 | -------------------------------------------------------------------------------- /example/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('bounty:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purser-example", 3 | "description": "Example Node.js app that uses Purser", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "start": "node ./bin/www" 8 | }, 9 | "dependencies": { 10 | "ahoy.js": "^0.2.1", 11 | "body-parser": "~1.15.1", 12 | "cookie-parser": "~1.4.3", 13 | "debug": "~2.2.0", 14 | "express": "~4.13.4", 15 | "jade": "~1.11.0", 16 | "morgan": "~1.7.0", 17 | "serve-favicon": "~2.3.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/public/javascripts/purser.js: -------------------------------------------------------------------------------- 1 | (function (window) { 2 | "use strict"; 3 | var params = ["utm_source", "utm_medium", "utm_name", "utm_term", "utm_campaign", "utm_content"]; 4 | var purser = window.purser || window.Purser || { 5 | fetch: function(){ 6 | return JSON.parse(window.localStorage.getItem("purser_visitor")); 7 | }, 8 | destroy: function(){ 9 | return window.localStorage.removeItem("purser_visitor"); 10 | }, 11 | convert: function(obj) { 12 | var attributes = this.update(obj); 13 | attributes.converted_at = new Date().toISOString(); 14 | attributes.conversion_page = window.location.origin + window.location.pathname; 15 | attributes.visits_at_conversion = (attributes.visits || []).length; 16 | attributes.pageviews_before_conversion = attributes.pageviews || 0; 17 | window.localStorage.setItem("purser_visitor", JSON.stringify(attributes)); 18 | return attributes; 19 | }, 20 | update: function(obj) { 21 | var attributes = this.fetch(); 22 | if (!attributes) { attributes = purser.create(); } 23 | for (var key in obj) { 24 | if (obj.hasOwnProperty(key)) { 25 | attributes[key] = obj[key]; 26 | } 27 | } 28 | window.localStorage.setItem("purser_visitor", JSON.stringify(attributes)); 29 | return attributes; 30 | }, 31 | createInstance: function() { 32 | var attributes = { 33 | referrer: document.referrer.length ? document.referrer : "direct", 34 | browser_timezone: new Date().getTimezoneOffset()/60, 35 | browser_language: window.navigator.language, 36 | landing_page: window.location.origin + window.location.pathname, 37 | screen_height: window.screen.height, 38 | screen_width: window.screen.width, 39 | }; 40 | for (var i = 0; i < params.length; i++) { 41 | var param = params[i]; 42 | param = param.replace(/[\[\]]/g, "\\$&"); 43 | var regex = new RegExp("[?&]" + param + "(=([^&#]*)|&|#|$)"); 44 | var results = regex.exec(window.location.href); 45 | if (results && results[2]) attributes[param] = decodeURIComponent(results[2].replace(/\+/g, " ")); 46 | } 47 | return attributes; 48 | }, 49 | create: function() { 50 | var attributes = this.createInstance(); 51 | attributes.last_visit = parseInt(new Date().getTime()/1000); 52 | attributes.pageviews = 1; 53 | attributes.first_website_visit = new Date().toISOString(); 54 | window.localStorage.setItem("purser_visitor", JSON.stringify(attributes)); 55 | return attributes; 56 | }, 57 | visits: { 58 | recently: function() { 59 | var attributes = purser.fetch(); 60 | if (!attributes.last_visit) return false; 61 | var timeDiffInHours = (parseInt(new Date().getTime()/1000) - attributes.last_visit)/3600; 62 | return timeDiffInHours < 0.5; // last visited less than half an hour ago. 63 | }, 64 | create: function() { 65 | var attributes = purser.fetch(); 66 | attributes.visits = attributes.visits || []; 67 | var visit = purser.createInstance(); 68 | visit.id = (((1+Math.random())*0x10000)|0).toString(16).substring(1); 69 | visit.date = new Date().toISOString(); 70 | attributes.visits.push(visit); 71 | attributes.last_visit = parseInt(new Date().getTime()/1000); 72 | purser.update(attributes); 73 | return attributes; 74 | }, 75 | fetch: function(id) { 76 | var attributes = purser.fetch(); 77 | var visit = attributes.visits.filter(function(visit) { 78 | return visit.id === id; 79 | })[0]; 80 | visit.index = attributes.visits.map(function(visit) { 81 | return visit.id; 82 | }).indexOf(id); 83 | return visit; 84 | }, 85 | update: function(id, obj) { 86 | var visit = purser.visits.fetch(id); 87 | var attributes = purser.fetch(); 88 | 89 | for (var key in obj) { 90 | if (obj.hasOwnProperty(key)) { 91 | visit[key] = obj[key]; 92 | } 93 | } 94 | attributes.visits[visit.index] = visit; 95 | return purser.update(attributes); 96 | }, 97 | delete: function(id) { 98 | var attributes = purser.fetch(); 99 | var visit = purser.visits.fetch(id); 100 | attributes.visits = attributes.visits.splice(visit.index, 1); 101 | return purser.update(attributes); 102 | }, 103 | all: function() { 104 | var attributes = purser.fetch(); 105 | return attributes.visits || []; 106 | } 107 | }, 108 | pageviews: { 109 | add: function() { 110 | var attributes = purser.fetch(); 111 | attributes.pageviews = attributes.pageviews + 1 || 1; 112 | return purser.update(attributes); 113 | } 114 | } 115 | }; 116 | if (!purser.fetch()) { 117 | purser.create(); 118 | } else { 119 | if (!purser.visits.recently()) { 120 | purser.visits.create(); 121 | } 122 | purser.pageviews.add(); 123 | } 124 | window.purser = purser; 125 | }(window)); 126 | -------------------------------------------------------------------------------- /example/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /example/routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | router.get('/', function(req, res, next) { 5 | res.render('index', {title: "Purser"}) 6 | }); 7 | 8 | router.post('/test', function(req, res, next) { 9 | console.log(req.body) 10 | res.send(req.body) 11 | }); 12 | 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /example/views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /example/views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p #{title} is a JavaScript library for preserving user data from first website visit to signup. 6 | p This enables you to associate signups to marketing campaigns, referrers, and more. 7 | h3 How it works 8 | p The library stores data such as utm_medium, landing_page and referrer in a localStorage object, and makes the object available via a handy API with the following methods: 9 | pre #{title}.create() // automatically called on first website visit 10 | | #{title}.fetch() // returns the object 11 | | #{title}.convert(obj) // returns the object updated with conversion data 12 | | #{title}.update(obj) // lets you add additional parameters to the object 13 | | #{title}.destroy() // removes the object from localStorage 14 | | #{title}.visits.create() // adds a visit instance to the object, called automatically if has been over 30 minutes since the most recent visit. 15 | | #{title}.visits.all() // returns an array of all visits 16 | | #{title}.visits.fetch(id) // Fetch a specified visit 17 | | #{title}.visits.update(id, obj) // Update a specified visit with data in an object 18 | | #{title}.visits.delete(id) // Delete a specified visit 19 | 20 | h3 Usage 21 | p Add the library to every page on your website. When a visitor creates an account, call purser.convert() and add the user to your CRM/ChartMogul with attributes returned. 22 | ol 23 | li Install the #{purser} library 24 | li When a visitor creates an account, call purser.convert(obj) to get the visitor's marketing attributes 25 | li Add the user's marketing attributes to them in your CRM or app 26 | a(href="https://github.com/bilbof/purser").btn.btn-text.btn-text--more View the Github repository 27 | h3 Demo 28 | p Enter your name and click the 'Demo Create Account' button to see an example of how #{title} works. 29 | .demo 30 | form 31 | .form-group 32 | input( type="text" placeholder="Your Name" required).form-control#name 33 | .form-group 34 | Demo Create Account 35 | Add a visit 36 | Delete object 37 | pre#result 38 | 39 | script(src="/javascripts/purser.js") 40 | script. 41 | var submit = document.getElementById('test'); 42 | var visit = document.getElementById('test-visit'); 43 | var remove = document.getElementById('test-delete'); 44 | 45 | function post(data){ 46 | var xhr = new XMLHttpRequest(); 47 | xhr.open("POST", "/test"); 48 | xhr.setRequestHeader("Content-Type", "application/json"); 49 | xhr.onreadystatechange = function() { 50 | if(xhr.readyState == 4 && xhr.status == 200) { 51 | document.getElementById('result').innerText = JSON.stringify(JSON.parse(xhr.responseText), null, '\t') 52 | } 53 | } 54 | 55 | xhr.send(JSON.stringify(data)); 56 | } 57 | 58 | submit.addEventListener('click', function(e){ 59 | e.preventDefault(); 60 | post({ 61 | name: document.getElementById('name').value || "Not given", 62 | lead_created_at: new Date().toISOString(), 63 | purser_attributes: purser.convert({ 64 | sign_up_button: "green-button" 65 | }) 66 | }) 67 | }) 68 | 69 | visit.addEventListener('click', function(e){ 70 | e.preventDefault(); 71 | post({ 72 | name: document.getElementById('name').value || "Not given", 73 | visit_button_clicked_at: new Date().toISOString(), 74 | purser_attributes: purser.visits.create() 75 | }); 76 | }) 77 | 78 | remove.addEventListener('click', function(e){ 79 | e.preventDefault(); 80 | purser.destroy(); 81 | document.getElementById('result').innerText = "" 82 | }) 83 | -------------------------------------------------------------------------------- /example/views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | link(rel='stylesheet', href="https://chartmogul.com/assets/css/chartmogul-2b28fbc207.min.css") 7 | body 8 | block content 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Purser 5 | 6 | 7 | 11 | 12 | 13 |

Purser

14 |

Welcome to Purser! Open your browser console to see the data collected by Purser.

15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purser", 3 | "version": "1.1.0", 4 | "description": "A lightweight JavaScript library for preserving user data from first website visit to signup.", 5 | "main": "pursor.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Bill Franklin ", 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /purser.js: -------------------------------------------------------------------------------- 1 | (function (window) { 2 | "use strict"; 3 | var params = ["utm_source", "utm_medium", "utm_name", "utm_term", "utm_campaign", "utm_content"]; 4 | var purser = window.purser || window.Purser || { 5 | fetch: function(){ 6 | return JSON.parse(window.localStorage.getItem("purser_visitor")); 7 | }, 8 | destroy: function(){ 9 | return window.localStorage.removeItem("purser_visitor"); 10 | }, 11 | convert: function(obj) { 12 | var attributes = this.update(obj); 13 | attributes.converted_at = new Date().toISOString(); 14 | attributes.conversion_page = window.location.origin + window.location.pathname; 15 | attributes.visits_at_conversion = (attributes.visits || []).length; 16 | attributes.pageviews_before_conversion = attributes.pageviews || 0; 17 | window.localStorage.setItem("purser_visitor", JSON.stringify(attributes)); 18 | return attributes; 19 | }, 20 | update: function(obj) { 21 | var attributes = this.fetch(); 22 | if (!attributes) { attributes = purser.create(); } 23 | for (var key in obj) { 24 | if (obj.hasOwnProperty(key)) { 25 | attributes[key] = obj[key]; 26 | } 27 | } 28 | window.localStorage.setItem("purser_visitor", JSON.stringify(attributes)); 29 | return attributes; 30 | }, 31 | createInstance: function() { 32 | var attributes = { 33 | referrer: document.referrer.length ? document.referrer : "direct", 34 | browser_timezone: new Date().getTimezoneOffset()/60, 35 | browser_language: window.navigator.language, 36 | landing_page: window.location.origin + window.location.pathname, 37 | screen_height: window.screen.height, 38 | screen_width: window.screen.width, 39 | }; 40 | for (var i = 0; i < params.length; i++) { 41 | var param = params[i]; 42 | param = param.replace(/[\[\]]/g, "\\$&"); 43 | var regex = new RegExp("[?&]" + param + "(=([^&#]*)|&|#|$)"); 44 | var results = regex.exec(window.location.href); 45 | if (results && results[2]) attributes[param] = decodeURIComponent(results[2].replace(/\+/g, " ")); 46 | } 47 | return attributes; 48 | }, 49 | create: function() { 50 | var attributes = this.createInstance(); 51 | attributes.last_visit = parseInt(new Date().getTime()/1000); 52 | attributes.pageviews = 1; 53 | attributes.first_website_visit = new Date().toISOString(); 54 | window.localStorage.setItem("purser_visitor", JSON.stringify(attributes)); 55 | return attributes; 56 | }, 57 | visits: { 58 | recently: function() { 59 | var attributes = purser.fetch(); 60 | if (!attributes.last_visit) return false; 61 | var timeDiffInHours = (parseInt(new Date().getTime()/1000) - attributes.last_visit)/3600; 62 | return timeDiffInHours < 0.5; // last visited less than half an hour ago. 63 | }, 64 | create: function() { 65 | var attributes = purser.fetch(); 66 | attributes.visits = attributes.visits || []; 67 | var visit = purser.createInstance(); 68 | visit.id = (((1+Math.random())*0x10000)|0).toString(16).substring(1); 69 | visit.date = new Date().toISOString(); 70 | attributes.visits.push(visit); 71 | attributes.last_visit = parseInt(new Date().getTime()/1000); 72 | purser.update(attributes); 73 | return attributes; 74 | }, 75 | fetch: function(id) { 76 | var attributes = purser.fetch(); 77 | var visit = attributes.visits.filter(function(visit) { 78 | return visit.id === id; 79 | })[0]; 80 | visit.index = attributes.visits.map(function(visit) { 81 | return visit.id; 82 | }).indexOf(id); 83 | return visit; 84 | }, 85 | update: function(id, obj) { 86 | var visit = purser.visits.fetch(id); 87 | var attributes = purser.fetch(); 88 | 89 | for (var key in obj) { 90 | if (obj.hasOwnProperty(key)) { 91 | visit[key] = obj[key]; 92 | } 93 | } 94 | attributes.visits[visit.index] = visit; 95 | return purser.update(attributes); 96 | }, 97 | delete: function(id) { 98 | var attributes = purser.fetch(); 99 | var visit = purser.visits.fetch(id); 100 | attributes.visits = attributes.visits.splice(visit.index, 1); 101 | return purser.update(attributes); 102 | }, 103 | all: function() { 104 | var attributes = purser.fetch(); 105 | return attributes.visits || []; 106 | } 107 | }, 108 | pageviews: { 109 | add: function() { 110 | var attributes = purser.fetch(); 111 | attributes.pageviews = attributes.pageviews + 1 || 1; 112 | return purser.update(attributes); 113 | } 114 | } 115 | }; 116 | if (!purser.fetch()) { 117 | purser.create(); 118 | } else { 119 | if (!purser.visits.recently()) { 120 | purser.visits.create(); 121 | } 122 | purser.pageviews.add(); 123 | } 124 | window.purser = purser; 125 | }(window)); 126 | -------------------------------------------------------------------------------- /purser.min.js: -------------------------------------------------------------------------------- 1 | !function(e){"use strict";var t=["utm_source","utm_medium","utm_name","utm_term","utm_campaign","utm_content"],r=e.purser||e.Purser||{fetch:function(){return JSON.parse(e.localStorage.getItem("purser_visitor"))},destroy:function(){return e.localStorage.removeItem("purser_visitor")},convert:function(t){var r=this.update(t);return r.converted_at=(new Date).toISOString(),r.conversion_page=e.location.origin+e.location.pathname,r.visits_at_conversion=(r.visits||[]).length,r.pageviews_before_conversion=r.pageviews||0,e.localStorage.setItem("purser_visitor",JSON.stringify(r)),r},update:function(t){var i=this.fetch();i||(i=r.create());for(var n in t)t.hasOwnProperty(n)&&(i[n]=t[n]);return e.localStorage.setItem("purser_visitor",JSON.stringify(i)),i},createInstance:function(){for(var r={referrer:document.referrer.length?document.referrer:"direct",browser_timezone:(new Date).getTimezoneOffset()/60,browser_language:e.navigator.language,landing_page:e.location.origin+e.location.pathname,screen_height:e.screen.height,screen_width:e.screen.width},i=0;i