trackly-sdk 0.1.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/README.md +188 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +17 -0
- package/dist/queue.d.ts +37 -0
- package/dist/queue.js +102 -0
- package/dist/tracker.d.ts +42 -0
- package/dist/tracker.js +143 -0
- package/dist/transport.d.ts +19 -0
- package/dist/transport.js +85 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.js +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Analytics SDK
|
|
2
|
+
|
|
3
|
+
SDK leve de analytics para tracking de eventos no browser.
|
|
4
|
+
|
|
5
|
+
## 🚀 Instalação
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @analytics/sdk
|
|
9
|
+
# ou
|
|
10
|
+
npm install @analytics/sdk
|
|
11
|
+
# ou
|
|
12
|
+
yarn add @analytics/sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 📦 Uso
|
|
16
|
+
|
|
17
|
+
### Inicialização
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { Analytics } from "@analytics/sdk";
|
|
21
|
+
|
|
22
|
+
const analytics = new Analytics({
|
|
23
|
+
apiUrl: "https://api.seuapp.com/events",
|
|
24
|
+
batchSize: 10, // opcional: eventos por batch (padrão: 10)
|
|
25
|
+
flushInterval: 5000, // opcional: intervalo de envio em ms (padrão: 5000)
|
|
26
|
+
maxRetries: 3, // opcional: tentativas em caso de erro (padrão: 3)
|
|
27
|
+
debug: false, // opcional: ativar logs de debug (padrão: false)
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Tracking de Eventos
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// Evento customizado
|
|
35
|
+
analytics.track("button_clicked", {
|
|
36
|
+
button: "signup",
|
|
37
|
+
page: "homepage",
|
|
38
|
+
variant: "primary",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Pageview (automático por padrão, mas pode chamar manualmente)
|
|
42
|
+
analytics.pageview({
|
|
43
|
+
category: "blog",
|
|
44
|
+
author: "John Doe",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Identificar usuário
|
|
48
|
+
analytics.identify("user_123", {
|
|
49
|
+
email: "usuario@exemplo.com",
|
|
50
|
+
name: "João Silva",
|
|
51
|
+
plan: "premium",
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Exemplos Práticos
|
|
56
|
+
|
|
57
|
+
#### E-commerce
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Produto visualizado
|
|
61
|
+
analytics.track("product_viewed", {
|
|
62
|
+
product_id: "SKU-123",
|
|
63
|
+
name: "Tênis Running",
|
|
64
|
+
price: 299.9,
|
|
65
|
+
category: "Esportes",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Item adicionado ao carrinho
|
|
69
|
+
analytics.track("cart_add", {
|
|
70
|
+
product_id: "SKU-123",
|
|
71
|
+
quantity: 1,
|
|
72
|
+
price: 299.9,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Compra finalizada
|
|
76
|
+
analytics.track("purchase", {
|
|
77
|
+
order_id: "ORD-456",
|
|
78
|
+
total: 299.9,
|
|
79
|
+
currency: "BRL",
|
|
80
|
+
items: 1,
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### SaaS
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// Cadastro
|
|
88
|
+
analytics.identify("user_789", {
|
|
89
|
+
email: "user@startup.com",
|
|
90
|
+
company: "Startup Inc",
|
|
91
|
+
plan: "trial",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Feature utilizada
|
|
95
|
+
analytics.track("feature_used", {
|
|
96
|
+
feature: "export_data",
|
|
97
|
+
format: "csv",
|
|
98
|
+
rows: 1500,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Upgrade de plano
|
|
102
|
+
analytics.track("plan_upgraded", {
|
|
103
|
+
from: "trial",
|
|
104
|
+
to: "premium",
|
|
105
|
+
mrr: 99.0,
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 🔧 API
|
|
110
|
+
|
|
111
|
+
### `Analytics`
|
|
112
|
+
|
|
113
|
+
#### Métodos
|
|
114
|
+
|
|
115
|
+
- **`track(eventName: string, properties?: EventProperties)`**
|
|
116
|
+
Rastreia um evento customizado
|
|
117
|
+
|
|
118
|
+
- **`pageview(properties?: EventProperties)`**
|
|
119
|
+
Rastreia visualização de página
|
|
120
|
+
|
|
121
|
+
- **`identify(userId: string, traits?: UserTraits)`**
|
|
122
|
+
Identifica o usuário
|
|
123
|
+
|
|
124
|
+
- **`flush(): Promise<void>`**
|
|
125
|
+
Força envio imediato dos eventos na fila
|
|
126
|
+
|
|
127
|
+
- **`getUserId(): string | undefined`**
|
|
128
|
+
Retorna o userId atual
|
|
129
|
+
|
|
130
|
+
- **`getAnonymousId(): string`**
|
|
131
|
+
Retorna o ID anônimo (persistido no localStorage)
|
|
132
|
+
|
|
133
|
+
- **`shutdown(): void`**
|
|
134
|
+
Para o SDK e limpa recursos
|
|
135
|
+
|
|
136
|
+
## 🎯 Características
|
|
137
|
+
|
|
138
|
+
- ✅ **Leve**: Sem dependências externas
|
|
139
|
+
- ✅ **Type-safe**: Totalmente tipado com TypeScript
|
|
140
|
+
- ✅ **Batch automático**: Agrupa eventos para reduzir requests
|
|
141
|
+
- ✅ **Retry automático**: Reenvio em caso de falha (exponential backoff)
|
|
142
|
+
- ✅ **sendBeacon**: Garante envio antes de sair da página
|
|
143
|
+
- ✅ **Contexto automático**: Captura URL, referrer, user agent, etc
|
|
144
|
+
- ✅ **Anonymous ID**: Tracking persistente antes da identificação
|
|
145
|
+
|
|
146
|
+
## 🏗️ Build
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pnpm run build
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Gera:
|
|
153
|
+
|
|
154
|
+
- `dist/index.js` (CommonJS)
|
|
155
|
+
- `dist/index.esm.js` (ES Modules)
|
|
156
|
+
- `dist/index.d.ts` (TypeScript definitions)
|
|
157
|
+
|
|
158
|
+
## 📊 Formato dos Eventos
|
|
159
|
+
|
|
160
|
+
Todos os eventos enviados seguem esta estrutura:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
{
|
|
164
|
+
type: 'track' | 'page' | 'identify',
|
|
165
|
+
timestamp: 1234567890,
|
|
166
|
+
event?: 'button_clicked', // apenas para type='track'
|
|
167
|
+
properties?: { /* dados custom */ },
|
|
168
|
+
userId?: 'user_123',
|
|
169
|
+
anonymousId: 'anon_xxx',
|
|
170
|
+
traits?: { /* dados do usuário */ }, // apenas para type='identify'
|
|
171
|
+
context: {
|
|
172
|
+
page: {
|
|
173
|
+
url: 'https://...',
|
|
174
|
+
path: '/about',
|
|
175
|
+
title: 'Sobre',
|
|
176
|
+
referrer: 'https://...'
|
|
177
|
+
},
|
|
178
|
+
userAgent: '...',
|
|
179
|
+
locale: 'pt-BR',
|
|
180
|
+
timezone: 'America/Sao_Paulo',
|
|
181
|
+
screen: { width: 1920, height: 1080 }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## 📝 Licença
|
|
187
|
+
|
|
188
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics SDK - Event tracking para browser
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { Analytics } from '@analytics/sdk';
|
|
7
|
+
*
|
|
8
|
+
* const analytics = new Analytics({
|
|
9
|
+
* apiUrl: 'https://api.myapp.com/events'
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* analytics.track('button_clicked', { button: 'signup' });
|
|
13
|
+
* analytics.pageview();
|
|
14
|
+
* analytics.identify('user123', { email: 'user@example.com' });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export { Analytics } from "./tracker";
|
|
18
|
+
export type { AnalyticsConfig, AnalyticsEvent, EventProperties, UserTraits, EventContext, SendResult, } from "./types";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics SDK - Event tracking para browser
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { Analytics } from '@analytics/sdk';
|
|
7
|
+
*
|
|
8
|
+
* const analytics = new Analytics({
|
|
9
|
+
* apiUrl: 'https://api.myapp.com/events'
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* analytics.track('button_clicked', { button: 'signup' });
|
|
13
|
+
* analytics.pageview();
|
|
14
|
+
* analytics.identify('user123', { email: 'user@example.com' });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export { Analytics } from "./tracker";
|
package/dist/queue.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AnalyticsEvent } from "./types";
|
|
2
|
+
import { Transport } from "./transport";
|
|
3
|
+
/**
|
|
4
|
+
* Fila de eventos com auto-flush
|
|
5
|
+
*/
|
|
6
|
+
export declare class EventQueue {
|
|
7
|
+
private queue;
|
|
8
|
+
private transport;
|
|
9
|
+
private batchSize;
|
|
10
|
+
private flushInterval;
|
|
11
|
+
private flushTimer?;
|
|
12
|
+
private isFlushing;
|
|
13
|
+
constructor(transport: Transport, batchSize?: number, flushInterval?: number);
|
|
14
|
+
/**
|
|
15
|
+
* Adiciona evento à fila
|
|
16
|
+
*/
|
|
17
|
+
enqueue(event: AnalyticsEvent): void;
|
|
18
|
+
/**
|
|
19
|
+
* Envia todos os eventos da fila
|
|
20
|
+
*/
|
|
21
|
+
flush(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Retorna número de eventos na fila
|
|
24
|
+
*/
|
|
25
|
+
size(): number;
|
|
26
|
+
/**
|
|
27
|
+
* Limpa a fila
|
|
28
|
+
*/
|
|
29
|
+
clear(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Para o flush timer
|
|
32
|
+
*/
|
|
33
|
+
stop(): void;
|
|
34
|
+
private startFlushTimer;
|
|
35
|
+
private flushViaBeacon;
|
|
36
|
+
private setupBeforeUnload;
|
|
37
|
+
}
|
package/dist/queue.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Fila de eventos com auto-flush
|
|
12
|
+
*/
|
|
13
|
+
export class EventQueue {
|
|
14
|
+
constructor(transport, batchSize = 10, flushInterval = 5000) {
|
|
15
|
+
this.queue = [];
|
|
16
|
+
this.isFlushing = false;
|
|
17
|
+
this.transport = transport;
|
|
18
|
+
this.batchSize = batchSize;
|
|
19
|
+
this.flushInterval = flushInterval;
|
|
20
|
+
// Auto-flush em intervalos regulares
|
|
21
|
+
this.startFlushTimer();
|
|
22
|
+
// Flush antes de sair da página
|
|
23
|
+
this.setupBeforeUnload();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Adiciona evento à fila
|
|
27
|
+
*/
|
|
28
|
+
enqueue(event) {
|
|
29
|
+
this.queue.push(event);
|
|
30
|
+
// Flush automático quando atingir batch size
|
|
31
|
+
if (this.queue.length >= this.batchSize) {
|
|
32
|
+
this.flush();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Envia todos os eventos da fila
|
|
37
|
+
*/
|
|
38
|
+
flush() {
|
|
39
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
40
|
+
if (this.isFlushing || this.queue.length === 0) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.isFlushing = true;
|
|
44
|
+
const events = [...this.queue];
|
|
45
|
+
this.queue = [];
|
|
46
|
+
try {
|
|
47
|
+
yield this.transport.send(events);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
// Em caso de erro, recoloca eventos na fila
|
|
51
|
+
this.queue.unshift(...events);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
this.isFlushing = false;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Retorna número de eventos na fila
|
|
60
|
+
*/
|
|
61
|
+
size() {
|
|
62
|
+
return this.queue.length;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Limpa a fila
|
|
66
|
+
*/
|
|
67
|
+
clear() {
|
|
68
|
+
this.queue = [];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Para o flush timer
|
|
72
|
+
*/
|
|
73
|
+
stop() {
|
|
74
|
+
if (this.flushTimer) {
|
|
75
|
+
clearInterval(this.flushTimer);
|
|
76
|
+
this.flushTimer = undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
startFlushTimer() {
|
|
80
|
+
this.flushTimer = window.setInterval(() => {
|
|
81
|
+
this.flush();
|
|
82
|
+
}, this.flushInterval);
|
|
83
|
+
}
|
|
84
|
+
flushViaBeacon() {
|
|
85
|
+
if (this.queue.length > 0) {
|
|
86
|
+
this.transport.sendBeacon([...this.queue]);
|
|
87
|
+
this.queue = [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
setupBeforeUnload() {
|
|
91
|
+
// Usa sendBeacon para garantir envio antes de sair
|
|
92
|
+
window.addEventListener("beforeunload", () => {
|
|
93
|
+
this.flushViaBeacon();
|
|
94
|
+
});
|
|
95
|
+
// Também flush em visibilitychange (mobile)
|
|
96
|
+
document.addEventListener("visibilitychange", () => {
|
|
97
|
+
if (document.visibilityState === "hidden") {
|
|
98
|
+
this.flushViaBeacon();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AnalyticsConfig, EventProperties, UserTraits } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Classe principal do Analytics SDK
|
|
4
|
+
*/
|
|
5
|
+
export declare class Analytics {
|
|
6
|
+
private config;
|
|
7
|
+
private transport;
|
|
8
|
+
private queue;
|
|
9
|
+
private userId?;
|
|
10
|
+
private userTraits?;
|
|
11
|
+
private anonymousId;
|
|
12
|
+
constructor(config: AnalyticsConfig);
|
|
13
|
+
/**
|
|
14
|
+
* Identifica o usuário
|
|
15
|
+
*/
|
|
16
|
+
identify(userId: string, traits?: UserTraits): void;
|
|
17
|
+
/**
|
|
18
|
+
* Rastreia um evento customizado
|
|
19
|
+
*/
|
|
20
|
+
track(eventName: string, properties?: EventProperties): void;
|
|
21
|
+
/**
|
|
22
|
+
* Rastreia visualização de página
|
|
23
|
+
*/
|
|
24
|
+
pageview(properties?: EventProperties): void;
|
|
25
|
+
/**
|
|
26
|
+
* Força envio imediato dos eventos na fila
|
|
27
|
+
*/
|
|
28
|
+
flush(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Retorna o userId atual
|
|
31
|
+
*/
|
|
32
|
+
getUserId(): string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Retorna o anonymousId
|
|
35
|
+
*/
|
|
36
|
+
getAnonymousId(): string;
|
|
37
|
+
/**
|
|
38
|
+
* Para o SDK e limpa recursos
|
|
39
|
+
*/
|
|
40
|
+
shutdown(): void;
|
|
41
|
+
private createAndEnqueueEvent;
|
|
42
|
+
}
|
package/dist/tracker.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { Transport } from "./transport";
|
|
11
|
+
import { EventQueue } from "./queue";
|
|
12
|
+
/**
|
|
13
|
+
* Gerador de ID anônimo
|
|
14
|
+
*/
|
|
15
|
+
function generateAnonymousId() {
|
|
16
|
+
return `anon_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Obtém ou cria anonymousId no localStorage
|
|
20
|
+
*/
|
|
21
|
+
function getAnonymousId() {
|
|
22
|
+
const key = "analytics_anonymous_id";
|
|
23
|
+
let id = localStorage.getItem(key);
|
|
24
|
+
if (!id) {
|
|
25
|
+
id = generateAnonymousId();
|
|
26
|
+
localStorage.setItem(key, id);
|
|
27
|
+
}
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Coleta contexto automático do evento
|
|
32
|
+
*/
|
|
33
|
+
function getEventContext() {
|
|
34
|
+
return {
|
|
35
|
+
page: {
|
|
36
|
+
url: window.location.href,
|
|
37
|
+
path: window.location.pathname,
|
|
38
|
+
title: document.title,
|
|
39
|
+
referrer: document.referrer,
|
|
40
|
+
},
|
|
41
|
+
userAgent: navigator.userAgent,
|
|
42
|
+
locale: navigator.language,
|
|
43
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
44
|
+
screen: {
|
|
45
|
+
width: window.screen.width,
|
|
46
|
+
height: window.screen.height,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Classe principal do Analytics SDK
|
|
52
|
+
*/
|
|
53
|
+
export class Analytics {
|
|
54
|
+
constructor(config) {
|
|
55
|
+
var _a, _b, _c, _d, _e;
|
|
56
|
+
// Configuração padrão
|
|
57
|
+
this.config = {
|
|
58
|
+
apiUrl: config.apiUrl,
|
|
59
|
+
batchSize: (_a = config.batchSize) !== null && _a !== void 0 ? _a : 10,
|
|
60
|
+
flushInterval: (_b = config.flushInterval) !== null && _b !== void 0 ? _b : 5000,
|
|
61
|
+
maxRetries: (_c = config.maxRetries) !== null && _c !== void 0 ? _c : 3,
|
|
62
|
+
debug: (_d = config.debug) !== null && _d !== void 0 ? _d : false,
|
|
63
|
+
disableAutoPageview: (_e = config.disableAutoPageview) !== null && _e !== void 0 ? _e : false,
|
|
64
|
+
};
|
|
65
|
+
this.anonymousId = getAnonymousId();
|
|
66
|
+
this.transport = new Transport(this.config.apiUrl, this.config.maxRetries, this.config.debug);
|
|
67
|
+
this.queue = new EventQueue(this.transport, this.config.batchSize, this.config.flushInterval);
|
|
68
|
+
// Pageview automático
|
|
69
|
+
if (!this.config.disableAutoPageview) {
|
|
70
|
+
this.pageview();
|
|
71
|
+
}
|
|
72
|
+
if (this.config.debug) {
|
|
73
|
+
console.log("[Analytics] Initialized with config:", this.config);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Identifica o usuário
|
|
78
|
+
*/
|
|
79
|
+
identify(userId, traits) {
|
|
80
|
+
this.userId = userId;
|
|
81
|
+
this.userTraits = traits;
|
|
82
|
+
this.createAndEnqueueEvent({
|
|
83
|
+
type: "identify",
|
|
84
|
+
userId,
|
|
85
|
+
traits,
|
|
86
|
+
}, `User identified: ${userId}`);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Rastreia um evento customizado
|
|
90
|
+
*/
|
|
91
|
+
track(eventName, properties) {
|
|
92
|
+
this.createAndEnqueueEvent({
|
|
93
|
+
type: "track",
|
|
94
|
+
event: eventName,
|
|
95
|
+
properties,
|
|
96
|
+
userId: this.userId,
|
|
97
|
+
}, `Event tracked: ${eventName}`, properties);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Rastreia visualização de página
|
|
101
|
+
*/
|
|
102
|
+
pageview(properties) {
|
|
103
|
+
this.createAndEnqueueEvent({
|
|
104
|
+
type: "page",
|
|
105
|
+
properties,
|
|
106
|
+
userId: this.userId,
|
|
107
|
+
}, "Pageview tracked");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Força envio imediato dos eventos na fila
|
|
111
|
+
*/
|
|
112
|
+
flush() {
|
|
113
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
114
|
+
yield this.queue.flush();
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Retorna o userId atual
|
|
119
|
+
*/
|
|
120
|
+
getUserId() {
|
|
121
|
+
return this.userId;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Retorna o anonymousId
|
|
125
|
+
*/
|
|
126
|
+
getAnonymousId() {
|
|
127
|
+
return this.anonymousId;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Para o SDK e limpa recursos
|
|
131
|
+
*/
|
|
132
|
+
shutdown() {
|
|
133
|
+
this.queue.stop();
|
|
134
|
+
this.queue.clear();
|
|
135
|
+
}
|
|
136
|
+
createAndEnqueueEvent(eventData, debugMessage, debugData) {
|
|
137
|
+
const event = Object.assign(Object.assign({}, eventData), { timestamp: Date.now(), anonymousId: this.anonymousId, context: getEventContext() });
|
|
138
|
+
this.queue.enqueue(event);
|
|
139
|
+
if (this.config.debug) {
|
|
140
|
+
console.log(`[Analytics] ${debugMessage}`, debugData !== undefined ? debugData : "");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AnalyticsEvent, SendResult } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Transport layer para envio de eventos
|
|
4
|
+
*/
|
|
5
|
+
export declare class Transport {
|
|
6
|
+
private apiUrl;
|
|
7
|
+
private maxRetries;
|
|
8
|
+
private debug;
|
|
9
|
+
constructor(apiUrl: string, maxRetries?: number, debug?: boolean);
|
|
10
|
+
/**
|
|
11
|
+
* Envia um batch de eventos para a API
|
|
12
|
+
*/
|
|
13
|
+
send(events: AnalyticsEvent[]): Promise<SendResult>;
|
|
14
|
+
/**
|
|
15
|
+
* Envia evento único usando sendBeacon (para unload)
|
|
16
|
+
*/
|
|
17
|
+
sendBeacon(events: AnalyticsEvent[]): boolean;
|
|
18
|
+
private sleep;
|
|
19
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Transport layer para envio de eventos
|
|
12
|
+
*/
|
|
13
|
+
export class Transport {
|
|
14
|
+
constructor(apiUrl, maxRetries = 3, debug = false) {
|
|
15
|
+
this.apiUrl = apiUrl;
|
|
16
|
+
this.maxRetries = maxRetries;
|
|
17
|
+
this.debug = debug;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Envia um batch de eventos para a API
|
|
21
|
+
*/
|
|
22
|
+
send(events) {
|
|
23
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
if (events.length === 0) {
|
|
25
|
+
return { success: true };
|
|
26
|
+
}
|
|
27
|
+
let lastError;
|
|
28
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
29
|
+
try {
|
|
30
|
+
if (this.debug) {
|
|
31
|
+
console.log(`[Analytics] Sending ${events.length} events (attempt ${attempt + 1})`);
|
|
32
|
+
}
|
|
33
|
+
const response = yield fetch(this.apiUrl, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: {
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify({ events }),
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
42
|
+
}
|
|
43
|
+
if (this.debug) {
|
|
44
|
+
console.log("[Analytics] Events sent successfully");
|
|
45
|
+
}
|
|
46
|
+
return { success: true };
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
50
|
+
if (this.debug) {
|
|
51
|
+
console.warn(`[Analytics] Send failed (attempt ${attempt + 1}):`, lastError.message);
|
|
52
|
+
}
|
|
53
|
+
// Aguarda antes de retry (exponential backoff)
|
|
54
|
+
if (attempt < this.maxRetries) {
|
|
55
|
+
yield this.sleep(Math.pow(2, attempt) * 1000);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { success: false, error: lastError };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Envia evento único usando sendBeacon (para unload)
|
|
64
|
+
*/
|
|
65
|
+
sendBeacon(events) {
|
|
66
|
+
if (events.length === 0 || !navigator.sendBeacon) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const blob = new Blob([JSON.stringify({ events })], {
|
|
71
|
+
type: "application/json",
|
|
72
|
+
});
|
|
73
|
+
return navigator.sendBeacon(this.apiUrl, blob);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (this.debug) {
|
|
77
|
+
console.warn("[Analytics] sendBeacon failed:", error);
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
sleep(ms) {
|
|
83
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
84
|
+
}
|
|
85
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Propriedades customizadas de um evento
|
|
3
|
+
*/
|
|
4
|
+
export type EventProperties = Record<string, any>;
|
|
5
|
+
/**
|
|
6
|
+
* Dados do usuário
|
|
7
|
+
*/
|
|
8
|
+
export interface UserTraits {
|
|
9
|
+
id?: string;
|
|
10
|
+
email?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Evento base
|
|
16
|
+
*/
|
|
17
|
+
export interface AnalyticsEvent {
|
|
18
|
+
type: "track" | "page" | "identify";
|
|
19
|
+
timestamp: number;
|
|
20
|
+
event?: string;
|
|
21
|
+
properties?: EventProperties;
|
|
22
|
+
userId?: string;
|
|
23
|
+
anonymousId?: string;
|
|
24
|
+
traits?: UserTraits;
|
|
25
|
+
context?: EventContext;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Contexto automático do evento
|
|
29
|
+
*/
|
|
30
|
+
export interface EventContext {
|
|
31
|
+
page?: {
|
|
32
|
+
url: string;
|
|
33
|
+
path: string;
|
|
34
|
+
title: string;
|
|
35
|
+
referrer: string;
|
|
36
|
+
};
|
|
37
|
+
userAgent?: string;
|
|
38
|
+
locale?: string;
|
|
39
|
+
timezone?: string;
|
|
40
|
+
screen?: {
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Configuração do SDK
|
|
47
|
+
*/
|
|
48
|
+
export interface AnalyticsConfig {
|
|
49
|
+
/** URL da API de coleta */
|
|
50
|
+
apiUrl: string;
|
|
51
|
+
/** Tamanho máximo do batch de eventos */
|
|
52
|
+
batchSize?: number;
|
|
53
|
+
/** Tempo máximo (ms) antes de enviar batch */
|
|
54
|
+
flushInterval?: number;
|
|
55
|
+
/** Número de tentativas em caso de falha */
|
|
56
|
+
maxRetries?: number;
|
|
57
|
+
/** Habilitar logs de debug */
|
|
58
|
+
debug?: boolean;
|
|
59
|
+
/** Desabilitar tracking automático de pageviews */
|
|
60
|
+
disableAutoPageview?: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Status de envio
|
|
64
|
+
*/
|
|
65
|
+
export interface SendResult {
|
|
66
|
+
success: boolean;
|
|
67
|
+
error?: Error;
|
|
68
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trackly-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SDK leve de analytics para tracking de eventos no browser",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"clean": "rm -rf dist"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"analytics",
|
|
18
|
+
"tracking",
|
|
19
|
+
"events",
|
|
20
|
+
"browser",
|
|
21
|
+
"typescript",
|
|
22
|
+
"zero-dependencies",
|
|
23
|
+
"batching",
|
|
24
|
+
"event-tracking"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/kaycfarias/trackly.git",
|
|
31
|
+
"directory": "packages/sdk"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/kaycfarias/trackly/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/kaycfarias/trackly/tree/main/packages/sdk#readme",
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^5.9.3"
|
|
39
|
+
}
|
|
40
|
+
}
|