spine-framework-cortex 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.json +2 -1
- package/package.json +9 -5
- package/public/spine-tracker.js +255 -0
- /package/{api → functions}/cortex-handler.ts +0 -0
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-framework-cortex",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Cortex — AI-powered support, CRM, and knowledge base app for Spine Framework",
|
|
6
6
|
"keywords": ["spine-framework", "crm", "support", "knowledge-base", "community"],
|
|
@@ -15,13 +15,17 @@
|
|
|
15
15
|
"seed/",
|
|
16
16
|
"pages/",
|
|
17
17
|
"components/",
|
|
18
|
-
"
|
|
18
|
+
"functions/",
|
|
19
19
|
"hooks/",
|
|
20
|
+
"public/",
|
|
20
21
|
"README.md"
|
|
21
22
|
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"validate": "npx spine-framework validate-app ."
|
|
25
|
+
},
|
|
22
26
|
"spine": {
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
27
|
+
"type": "app",
|
|
28
|
+
"slug": "cortex",
|
|
29
|
+
"manifestPath": "manifest.json"
|
|
26
30
|
}
|
|
27
31
|
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spine Marketing Site Tracker
|
|
3
|
+
* Tracks anonymous and identified visitors across spine-framework.com
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <script src="https://portal.spine-framework.com/spine-tracker.js" data-api-key="YOUR_KEY"></script>
|
|
7
|
+
* <script>
|
|
8
|
+
* spineTracker.init({
|
|
9
|
+
* apiUrl: 'https://portal.spine-framework.com/.netlify/functions/integration-routes?slug=funnel-signal',
|
|
10
|
+
* trackPageViews: true,
|
|
11
|
+
* trackClicks: true
|
|
12
|
+
* });
|
|
13
|
+
* </script>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
(function(window) {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const COOKIE_NAME_ANON = 'spine_anonymous_id';
|
|
20
|
+
const COOKIE_NAME_IDENTITY = 'spine_identity';
|
|
21
|
+
const STORAGE_KEY_ANON = 'spine_anonymous_id';
|
|
22
|
+
const COOKIE_DOMAIN = '.spine-framework.com';
|
|
23
|
+
const COOKIE_EXPIRY_DAYS = 365;
|
|
24
|
+
|
|
25
|
+
class SpineTracker {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.config = {
|
|
28
|
+
apiUrl: '',
|
|
29
|
+
apiKey: '',
|
|
30
|
+
trackPageViews: true,
|
|
31
|
+
trackClicks: true,
|
|
32
|
+
debug: false
|
|
33
|
+
};
|
|
34
|
+
this.identity = null;
|
|
35
|
+
this.anonymousId = null;
|
|
36
|
+
this.sessionId = this.generateId();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
generateId() {
|
|
40
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
41
|
+
return crypto.randomUUID();
|
|
42
|
+
}
|
|
43
|
+
return 'anon_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setCookie(name, value, days) {
|
|
47
|
+
const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString();
|
|
48
|
+
document.cookie = `${name}=${encodeURIComponent(JSON.stringify(value))};expires=${expires};domain=${COOKIE_DOMAIN};path=/;Secure;SameSite=Lax`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getCookie(name) {
|
|
52
|
+
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
|
53
|
+
if (match) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(decodeURIComponent(match[2]));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return match[2];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getOrCreateAnonymousId() {
|
|
64
|
+
// Try localStorage first
|
|
65
|
+
let id = localStorage.getItem(STORAGE_KEY_ANON);
|
|
66
|
+
|
|
67
|
+
// Fallback to cookie (for Safari ITP)
|
|
68
|
+
if (!id) {
|
|
69
|
+
const cookieId = this.getCookie(COOKIE_NAME_ANON);
|
|
70
|
+
if (cookieId) {
|
|
71
|
+
id = cookieId;
|
|
72
|
+
localStorage.setItem(STORAGE_KEY_ANON, id);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Generate new if not found
|
|
77
|
+
if (!id) {
|
|
78
|
+
id = this.generateId();
|
|
79
|
+
localStorage.setItem(STORAGE_KEY_ANON, id);
|
|
80
|
+
this.setCookie(COOKIE_NAME_ANON, id, COOKIE_EXPIRY_DAYS);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return id;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getIdentity() {
|
|
87
|
+
// Check for identity cookie
|
|
88
|
+
const identity = this.getCookie(COOKIE_NAME_IDENTITY);
|
|
89
|
+
if (identity && identity.account_id) {
|
|
90
|
+
return {
|
|
91
|
+
account_id: identity.account_id,
|
|
92
|
+
person_id: identity.person_id,
|
|
93
|
+
stage: 'identified'
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async sendSignal(actionType, actionValue, metadata = {}) {
|
|
100
|
+
if (!this.config.apiUrl || !this.config.apiKey) {
|
|
101
|
+
console.warn('[SpineTracker] Not initialized');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const identity = this.getIdentity();
|
|
106
|
+
const payload = {
|
|
107
|
+
source: 'mar',
|
|
108
|
+
stage: identity ? 'identified' : 'anonymous',
|
|
109
|
+
action_type: actionType,
|
|
110
|
+
action_value: actionValue,
|
|
111
|
+
session_id: this.sessionId,
|
|
112
|
+
url: window.location.href,
|
|
113
|
+
path: window.location.pathname,
|
|
114
|
+
referrer: document.referrer || null,
|
|
115
|
+
user_agent: navigator.userAgent,
|
|
116
|
+
...metadata
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (identity) {
|
|
120
|
+
payload.account_id = identity.account_id;
|
|
121
|
+
payload.person_id = identity.person_id;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Always include anonymous_id for session continuity
|
|
125
|
+
payload.anonymous_id = this.getOrCreateAnonymousId();
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const response = await fetch(this.config.apiUrl, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: {
|
|
131
|
+
'Content-Type': 'application/json',
|
|
132
|
+
'x-api-key': this.config.apiKey
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify(payload)
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (this.config.debug) {
|
|
138
|
+
console.log('[SpineTracker] Signal sent:', payload);
|
|
139
|
+
console.log('[SpineTracker] Response:', response.status);
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (this.config.debug) {
|
|
143
|
+
console.error('[SpineTracker] Failed to send signal:', error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
trackPageView() {
|
|
149
|
+
this.sendSignal('page_view', 1, {
|
|
150
|
+
title: document.title,
|
|
151
|
+
utm_source: this.getQueryParam('utm_source'),
|
|
152
|
+
utm_medium: this.getQueryParam('utm_medium'),
|
|
153
|
+
utm_campaign: this.getQueryParam('utm_campaign')
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
trackClick(element, actionType, actionValue = 2) {
|
|
158
|
+
element.addEventListener('click', () => {
|
|
159
|
+
this.sendSignal(actionType, actionValue, {
|
|
160
|
+
element_id: element.id || null,
|
|
161
|
+
element_text: element.innerText?.substring(0, 100) || null
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
getQueryParam(name) {
|
|
167
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
168
|
+
return urlParams.get(name);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
init(config) {
|
|
172
|
+
this.config = { ...this.config, ...config };
|
|
173
|
+
|
|
174
|
+
// Get API key from script tag if not provided
|
|
175
|
+
if (!this.config.apiKey) {
|
|
176
|
+
const script = document.querySelector('script[src*="spine-tracker.js"]');
|
|
177
|
+
if (script) {
|
|
178
|
+
this.config.apiKey = script.dataset.apiKey;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Initialize anonymous ID
|
|
183
|
+
this.anonymousId = this.getOrCreateAnonymousId();
|
|
184
|
+
|
|
185
|
+
// Check for identity
|
|
186
|
+
this.identity = this.getIdentity();
|
|
187
|
+
|
|
188
|
+
if (this.config.debug) {
|
|
189
|
+
console.log('[SpineTracker] Initialized:', {
|
|
190
|
+
anonymousId: this.anonymousId,
|
|
191
|
+
identity: this.identity,
|
|
192
|
+
sessionId: this.sessionId
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Track page view
|
|
197
|
+
if (this.config.trackPageViews) {
|
|
198
|
+
this.trackPageView();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Setup click tracking
|
|
202
|
+
if (this.config.trackClicks) {
|
|
203
|
+
this.setupClickTracking();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setupClickTracking() {
|
|
210
|
+
// Auto-track elements with data-track attributes
|
|
211
|
+
document.addEventListener('click', (e) => {
|
|
212
|
+
const target = e.target.closest('[data-track-action]');
|
|
213
|
+
if (target) {
|
|
214
|
+
const actionType = target.dataset.trackAction;
|
|
215
|
+
const actionValue = parseInt(target.dataset.trackValue) || 2;
|
|
216
|
+
this.sendSignal(actionType, actionValue, {
|
|
217
|
+
element_id: target.id || null,
|
|
218
|
+
element_text: target.innerText?.substring(0, 100) || null
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Public API for manual tracking
|
|
225
|
+
track(actionType, actionValue, metadata) {
|
|
226
|
+
return this.sendSignal(actionType, actionValue, metadata);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Update identity (called after login/signup)
|
|
230
|
+
setIdentity(accountId, personId) {
|
|
231
|
+
this.setCookie(COOKIE_NAME_IDENTITY, {
|
|
232
|
+
account_id: accountId,
|
|
233
|
+
person_id: personId,
|
|
234
|
+
set_at: new Date().toISOString()
|
|
235
|
+
}, COOKIE_EXPIRY_DAYS);
|
|
236
|
+
this.identity = { account_id: accountId, person_id: personId };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Clear identity (called on logout)
|
|
240
|
+
clearIdentity() {
|
|
241
|
+
this.setCookie(COOKIE_NAME_IDENTITY, '', -1);
|
|
242
|
+
this.identity = null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Expose to window
|
|
247
|
+
window.SpineTracker = SpineTracker;
|
|
248
|
+
window.spineTracker = new SpineTracker();
|
|
249
|
+
|
|
250
|
+
// Auto-init if config present
|
|
251
|
+
if (window.spineTrackerConfig) {
|
|
252
|
+
window.spineTracker.init(window.spineTrackerConfig);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
})(window);
|
|
File without changes
|