spine-framework-cortex 0.2.6 → 0.2.7

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 CHANGED
@@ -2,7 +2,8 @@
2
2
  "name": "Cortex",
3
3
  "slug": "cortex",
4
4
  "description": "Unified workspace for CRM, Support, Community, and Knowledge Base",
5
- "version": "1.0.0",
5
+ "version": "0.2.7",
6
+ "app_type": "full",
6
7
  "required_roles": ["support"],
7
8
  "routes": [
8
9
  "/cortex",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework-cortex",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
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
- "api/",
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
- "app_slug": "cortex",
24
- "entry_point": "./index.tsx",
25
- "manifest": "./manifest.json"
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