@arthurreira/analytics 0.8.0 → 0.10.0

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/dist/client.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  interface AnalyticsProps {
2
2
  apiKey: string;
3
3
  apiUrl: string;
4
+ wsUrl?: string;
4
5
  }
5
- declare function Analytics({ apiKey, apiUrl }: AnalyticsProps): null;
6
+ declare function Analytics({ apiKey, apiUrl, wsUrl }: AnalyticsProps): null;
6
7
 
7
- declare function useAnalytics(apiUrl: string, apiKey: string): {
8
+ declare function useAnalytics(apiUrl: string, apiKey: string, options?: {
9
+ wsUrl?: string;
10
+ }): {
8
11
  trackPageview: (path: string) => void;
9
12
  trackClick: (e: MouseEvent, element: HTMLElement) => void;
10
13
  trackScroll: (depth: number) => void;
package/dist/client.js CHANGED
@@ -124,6 +124,60 @@ async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
124
124
  });
125
125
  }
126
126
 
127
+ // src/lib/presence.ts
128
+ var DEFAULT_WS_URL = "wss://edge.arthurreira.dev/realtime";
129
+ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
130
+ if (typeof WebSocket === "undefined") {
131
+ return { disconnect: () => {
132
+ } };
133
+ }
134
+ let ws = null;
135
+ let attempts = 0;
136
+ let intentionalClose = false;
137
+ let retryTimer = null;
138
+ let targetUrl;
139
+ try {
140
+ const u = new URL(wsUrl);
141
+ u.searchParams.set("api_key", apiKey);
142
+ u.searchParams.set("session_id", sessionId);
143
+ targetUrl = u.toString();
144
+ } catch {
145
+ return { disconnect: () => {
146
+ } };
147
+ }
148
+ function connect() {
149
+ try {
150
+ ws = new WebSocket(targetUrl);
151
+ } catch {
152
+ return;
153
+ }
154
+ ws.onopen = () => {
155
+ attempts = 0;
156
+ ws.send(JSON.stringify({ type: "join", api_key: apiKey, session_id: sessionId }));
157
+ };
158
+ ws.onclose = () => {
159
+ if (intentionalClose || attempts >= 3) return;
160
+ const delay = Math.pow(2, attempts) * 1e3;
161
+ attempts++;
162
+ retryTimer = setTimeout(connect, delay);
163
+ };
164
+ }
165
+ connect();
166
+ return {
167
+ disconnect: () => {
168
+ intentionalClose = true;
169
+ if (retryTimer !== null) {
170
+ clearTimeout(retryTimer);
171
+ retryTimer = null;
172
+ }
173
+ if (ws) {
174
+ ws.close();
175
+ ws = null;
176
+ }
177
+ }
178
+ };
179
+ }
180
+
127
181
  // src/hooks/useAnalytics.ts
128
182
  var SESSION_EXPIRY_MINUTES = 30;
129
183
  async function getOrCreateSession(apiUrl, apiKey, visitorId) {
@@ -142,24 +196,33 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
142
196
  localStorage.setItem("af_session_last_activity", String(now));
143
197
  return newSessionId;
144
198
  }
145
- function useAnalytics(apiUrl, apiKey) {
199
+ function useAnalytics(apiUrl, apiKey, options) {
146
200
  const sessionId = useRef(null);
147
201
  const pendingEvents = useRef([]);
148
202
  const pathname = useRef(typeof window !== "undefined" ? window?.location?.pathname ?? "" : "");
203
+ const presence = useRef(null);
149
204
  useEffect(() => {
150
205
  if (typeof window === "undefined") return;
151
206
  const visitor_id = localStorage.getItem("af_analytics_visitor_id") || crypto.randomUUID();
152
207
  localStorage.setItem("af_analytics_visitor_id", visitor_id);
208
+ let cancelled = false;
153
209
  getOrCreateSession(apiUrl, apiKey, visitor_id).then((id) => {
210
+ if (cancelled) return;
154
211
  sessionId.current = id;
212
+ presence.current = connectPresence(apiKey, id, options?.wsUrl);
155
213
  pendingEvents.current.forEach((fn) => fn());
156
214
  pendingEvents.current = [];
157
215
  });
216
+ return () => {
217
+ cancelled = true;
218
+ };
158
219
  }, []);
159
220
  useEffect(() => {
160
221
  if (typeof window === "undefined") return;
161
222
  const sendEnd = () => {
162
223
  if (!sessionId.current) return;
224
+ presence.current?.disconnect();
225
+ presence.current = null;
163
226
  navigator.sendBeacon(`${apiUrl}/sessions/${sessionId.current}/end`);
164
227
  localStorage.removeItem("af_session_id");
165
228
  localStorage.removeItem("af_session_last_activity");
@@ -209,8 +272,8 @@ function useAnalytics(apiUrl, apiKey) {
209
272
  }
210
273
 
211
274
  // src/components/Analytics.tsx
212
- function Analytics({ apiKey, apiUrl }) {
213
- const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackError: trackError2 } = useAnalytics(apiUrl, apiKey);
275
+ function Analytics({ apiKey, apiUrl, wsUrl }) {
276
+ const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackError: trackError2 } = useAnalytics(apiUrl, apiKey, { wsUrl });
214
277
  const lastTracked = useRef2(null);
215
278
  const lastScrollDepth = useRef2(0);
216
279
  const pathname = typeof window !== "undefined" ? window.location.pathname : "";
package/dist/index.d.ts CHANGED
@@ -7,4 +7,10 @@ declare function trackSearch(apiUrl: string, apiKey: string, sessionId: string,
7
7
  declare function trackError(apiUrl: string, apiKey: string, sessionId: string, path: string, error: Error): Promise<void>;
8
8
  declare function trackCTA(apiUrl: string, apiKey: string, sessionId: string, path: string, ctaId: string, ctaVariant?: string): Promise<void>;
9
9
 
10
- export { createSession, trackCTA, trackClick, trackCopy, trackError, trackPageview, trackScroll, trackSearch };
10
+ declare const DEFAULT_WS_URL = "wss://edge.arthurreira.dev/realtime";
11
+ interface PresenceConnection {
12
+ disconnect: () => void;
13
+ }
14
+ declare function connectPresence(apiKey: string, sessionId: string, wsUrl?: string): PresenceConnection;
15
+
16
+ export { DEFAULT_WS_URL, type PresenceConnection, connectPresence, createSession, trackCTA, trackClick, trackCopy, trackError, trackPageview, trackScroll, trackSearch };
package/dist/index.js CHANGED
@@ -114,7 +114,63 @@ async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
114
114
  cta_variant: ctaVariant || null
115
115
  });
116
116
  }
117
+
118
+ // src/lib/presence.ts
119
+ var DEFAULT_WS_URL = "wss://edge.arthurreira.dev/realtime";
120
+ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
121
+ if (typeof WebSocket === "undefined") {
122
+ return { disconnect: () => {
123
+ } };
124
+ }
125
+ let ws = null;
126
+ let attempts = 0;
127
+ let intentionalClose = false;
128
+ let retryTimer = null;
129
+ let targetUrl;
130
+ try {
131
+ const u = new URL(wsUrl);
132
+ u.searchParams.set("api_key", apiKey);
133
+ u.searchParams.set("session_id", sessionId);
134
+ targetUrl = u.toString();
135
+ } catch {
136
+ return { disconnect: () => {
137
+ } };
138
+ }
139
+ function connect() {
140
+ try {
141
+ ws = new WebSocket(targetUrl);
142
+ } catch {
143
+ return;
144
+ }
145
+ ws.onopen = () => {
146
+ attempts = 0;
147
+ ws.send(JSON.stringify({ type: "join", api_key: apiKey, session_id: sessionId }));
148
+ };
149
+ ws.onclose = () => {
150
+ if (intentionalClose || attempts >= 3) return;
151
+ const delay = Math.pow(2, attempts) * 1e3;
152
+ attempts++;
153
+ retryTimer = setTimeout(connect, delay);
154
+ };
155
+ }
156
+ connect();
157
+ return {
158
+ disconnect: () => {
159
+ intentionalClose = true;
160
+ if (retryTimer !== null) {
161
+ clearTimeout(retryTimer);
162
+ retryTimer = null;
163
+ }
164
+ if (ws) {
165
+ ws.close();
166
+ ws = null;
167
+ }
168
+ }
169
+ };
170
+ }
117
171
  export {
172
+ DEFAULT_WS_URL,
173
+ connectPresence,
118
174
  createSession,
119
175
  trackCTA,
120
176
  trackClick,
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  { "name": "@arthurreira/analytics",
2
- "version": "0.8.0",
2
+ "version": "0.10.0",
3
3
  "type": "module",
4
4
  "scripts": {
5
5
  "build": "tsup",