],
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