Skip to content

Commit f0c1933

Browse files
rmarsigliclaude
andcommitted
feat(sdk): implement full external SDK
Phase 2: Full External SDK (3h) - Replace stub with real implementation - Process queued events on load - Session management (sessionStorage + UUID) - send() function (sendBeacon + fetch fallback) - track() and trackPageview() methods - Auto-tracking (SPA navigation + outbound links) - Error handling (silent failures in production) - Debug mode logging - Idempotent init (prevent double initialization) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 9fc2bbd commit f0c1933

1 file changed

Lines changed: 202 additions & 0 deletions

File tree

sdk/rush.js

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
(function(window, document) {
2+
'use strict';
3+
4+
// Configuration defaults
5+
var defaults = {
6+
endpoint: 'https://analytics.example.com/ingest',
7+
autoTrack: true,
8+
debug: false
9+
};
10+
11+
var config = {};
12+
var sessionId = null;
13+
var initialized = false;
14+
15+
// Real Rush implementation (replaces stub)
16+
var Rush = {
17+
init: function(options) {
18+
if (initialized) {
19+
if (config.debug) console.warn('[Rush] Already initialized');
20+
return;
21+
}
22+
23+
config = Object.assign({}, defaults, options);
24+
sessionId = getOrCreateSession();
25+
initialized = true;
26+
27+
if (config.debug) {
28+
console.log('[Rush] Initialized', {
29+
siteId: config.siteId,
30+
endpoint: config.endpoint,
31+
sessionId: sessionId
32+
});
33+
}
34+
35+
if (config.autoTrack) {
36+
trackPageview();
37+
setupAutoTracking();
38+
}
39+
},
40+
41+
track: function(eventName, data) {
42+
if (!initialized) {
43+
console.error('[Rush] Not initialized. Call Rush.init() first.');
44+
return;
45+
}
46+
47+
var payload = {
48+
site_id: config.siteId,
49+
session_id: sessionId,
50+
event_name: eventName,
51+
path: window.location.pathname,
52+
referrer: document.referrer || null,
53+
timestamp: new Date().toISOString()
54+
};
55+
56+
// Merge custom data
57+
if (data) {
58+
Object.keys(data).forEach(function(key) {
59+
payload[key] = data[key];
60+
});
61+
}
62+
63+
send(payload);
64+
},
65+
66+
trackPageview: function() {
67+
this.track('pageview', {
68+
url: window.location.href,
69+
title: document.title
70+
});
71+
}
72+
};
73+
74+
// Session management
75+
function getOrCreateSession() {
76+
var key = '_rush_sid';
77+
var sid = sessionStorage.getItem(key);
78+
if (!sid) {
79+
sid = generateUUID();
80+
sessionStorage.setItem(key, sid);
81+
}
82+
return sid;
83+
}
84+
85+
function generateUUID() {
86+
// Use native crypto API if available
87+
if (crypto.randomUUID) {
88+
return crypto.randomUUID();
89+
}
90+
91+
// Fallback for older browsers
92+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
93+
var r = Math.random() * 16 | 0;
94+
var v = c === 'x' ? r : (r & 0x3 | 0x8);
95+
return v.toString(16);
96+
});
97+
}
98+
99+
// Send event to backend
100+
function send(payload) {
101+
var data = JSON.stringify(payload);
102+
103+
// Prefer sendBeacon (more reliable for page unload)
104+
if (navigator.sendBeacon) {
105+
var blob = new Blob([data], { type: 'application/json' });
106+
var sent = navigator.sendBeacon(config.endpoint, blob);
107+
108+
if (config.debug) {
109+
console.log('[Rush] Event sent via sendBeacon:', sent, payload);
110+
}
111+
112+
return;
113+
}
114+
115+
// Fallback to fetch
116+
fetch(config.endpoint, {
117+
method: 'POST',
118+
headers: { 'Content-Type': 'application/json' },
119+
body: data,
120+
keepalive: true
121+
})
122+
.then(function() {
123+
if (config.debug) console.log('[Rush] Event sent via fetch:', payload);
124+
})
125+
.catch(function(err) {
126+
if (config.debug) console.error('[Rush] Send failed:', err);
127+
// Fail silently in production
128+
});
129+
}
130+
131+
function trackPageview() {
132+
Rush.track('pageview', {
133+
url: window.location.href,
134+
title: document.title
135+
});
136+
}
137+
138+
// Auto-tracking setup
139+
function setupAutoTracking() {
140+
// SPA navigation (pushState, replaceState, popstate)
141+
var pushState = history.pushState;
142+
history.pushState = function() {
143+
pushState.apply(history, arguments);
144+
setTimeout(trackPageview, 0); // Async to let framework update
145+
};
146+
147+
var replaceState = history.replaceState;
148+
history.replaceState = function() {
149+
replaceState.apply(history, arguments);
150+
setTimeout(trackPageview, 0);
151+
};
152+
153+
window.addEventListener('popstate', trackPageview);
154+
155+
// Outbound links tracking
156+
document.addEventListener('click', function(e) {
157+
var link = e.target.closest('a');
158+
if (link && link.href && link.hostname !== window.location.hostname) {
159+
Rush.track('outbound_link', {
160+
url: link.href,
161+
text: link.textContent || link.innerText
162+
});
163+
}
164+
});
165+
166+
if (config.debug) {
167+
console.log('[Rush] Auto-tracking enabled (SPA + outbound links)');
168+
}
169+
}
170+
171+
// Process queued events from stub
172+
function processQueue() {
173+
var queue = window.Rush._queue || [];
174+
175+
if (config.debug && queue.length > 0) {
176+
console.log('[Rush] Processing', queue.length, 'queued events');
177+
}
178+
179+
queue.forEach(function(item) {
180+
var method = item[0];
181+
var args = item.slice(1);
182+
183+
if (Rush[method]) {
184+
Rush[method].apply(Rush, args);
185+
}
186+
});
187+
188+
// Clear queue
189+
window.Rush._queue = [];
190+
}
191+
192+
// Replace stub with real implementation
193+
window.Rush = Rush;
194+
195+
// Process queued events
196+
if (document.readyState === 'loading') {
197+
document.addEventListener('DOMContentLoaded', processQueue);
198+
} else {
199+
processQueue();
200+
}
201+
202+
})(window, document);

0 commit comments

Comments
 (0)