ninetwo-user-tracking 1.0.6 → 1.0.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/README.md ADDED
@@ -0,0 +1,213 @@
1
+
2
+ # NineTwo User Tracking
3
+
4
+ Pacote de abstração de Analytics para React e Next.js.
5
+ Facilita a implementação do **Google Tag Manager (GTM)** utilizando **Delegação de Eventos** para cliques (via atributos HTML) e **Intersection Observer** para visualizações e confirmação de leitura.
6
+
7
+ ## ✨ Funcionalidades
8
+
9
+ - 🚀 **Zero Boilerplate:** Rastreamento declarativo via atributos `data-nt-ut-*`.
10
+ - 🖱️ **Click Tracking Automático:** Listener global que captura cliques.
11
+ - 👁️ **View Tracking:** Dispara evento ao visualizar elemento.
12
+ - 📖 **Read Confirmation:** Dispara evento secundário automaticamente após 5s de visualização contínua.
13
+ - 💉 **GTM Injection:** Injeção segura do script do GTM.
14
+ - ⚡ **Next.js Ready:** Compatível com App Router (Providers Pattern).
15
+
16
+ ---
17
+
18
+ ## 📦 Instalação
19
+
20
+ npm install ninetwo_user_tracking
21
+ ---
22
+
23
+ ## 🚀 Configuração (Next.js 13+ App Router)
24
+
25
+ ### 1. Crie o componente `app/providers.tsx`
26
+
27
+ ```tsx
28
+ 'use client';
29
+
30
+ import { TrackingProvider } from 'ninetwo_user_tracking';
31
+
32
+ export function Providers({ children }: { children: React.ReactNode }) {
33
+ return (
34
+ <TrackingProvider
35
+ gtmId="GTM-SEU-ID-AQUI"
36
+ debug={process.env.NODE_ENV === 'development'}
37
+ >
38
+ {children}
39
+ </TrackingProvider>
40
+ );
41
+ }
42
+
43
+ ```
44
+
45
+ ### 2. Envolva o `app/layout.tsx`
46
+
47
+ ```tsx
48
+ import { Providers } from "./providers";
49
+ import "./globals.css";
50
+
51
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
52
+ return (
53
+ <html lang="pt-BR">
54
+ <body>
55
+ <Providers>{children}</Providers>
56
+ </body>
57
+ </html>
58
+ );
59
+ }
60
+
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 🖱️ Rastreamento de Cliques (Click)
66
+
67
+ Adicione os atributos `data-nt-ut-*` ao elemento interativo.
68
+
69
+ ```tsx
70
+ <button
71
+ className="btn-primary"
72
+ data-nt-ut-event="add_to_cart"
73
+ data-nt-ut-category="ecommerce"
74
+ data-nt-ut-label="tenis_nike_v2"
75
+ data-nt-ut-type="click" // Opcional (default: click)
76
+ >
77
+ Comprar Agora
78
+ </button>
79
+
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 👁️ Rastreamento de Visualização e Leitura (View/Read)
85
+
86
+ Use o componente `<TrackView>` para monitorar impressões.
87
+ **Novidade:** Se o usuário permanecer com o elemento visível por 5 segundos (padrão), um segundo evento `read_confirmation` será disparado.
88
+
89
+ ```tsx
90
+ import { TrackView } from 'ninetwo_user_tracking';
91
+
92
+ export default function BlogPost() {
93
+ return (
94
+ <TrackView
95
+ eventName="article_view"
96
+ category="blog"
97
+ label="como_aprender_react"
98
+ threshold={0.5} // 50% visível para disparar
99
+ readTime={5000} // (Opcional) Tempo em ms para confirmar leitura
100
+ >
101
+ <article>
102
+ <h1>Como aprender React</h1>
103
+ <p>Conteúdo do artigo...</p>
104
+ </article>
105
+ </TrackView>
106
+ );
107
+ }
108
+
109
+ ```
110
+
111
+ ### Comportamento dos Eventos
112
+
113
+ Neste exemplo acima, dois eventos serão enviados ao DataLayer:
114
+
115
+ 1. **Assim que aparecer:**
116
+ * event: `"article_view"`
117
+ * type: `"view"`
118
+
119
+
120
+ 2. **Após 5 segundos visível:**
121
+ * event: `"article_view"`
122
+ * type: `"read_confirmation"`
123
+
124
+
125
+ ---
126
+
127
+ Aqui está a documentação exclusiva para o rastreamento de **Submit de Formulários**, pronta para copiar e colar.
128
+
129
+ ---
130
+
131
+ ## 📝 Rastreamento de Formulários (Submit)
132
+
133
+ O pacote detecta automaticamente o envio de formulários através de **Event Delegation**.
134
+ Isso significa que você deve adicionar os atributos de rastreamento **diretamente na tag `<form>**`, e não no botão de enviar.
135
+
136
+ O evento será disparado tanto ao clicar no botão `type="submit"` quanto ao pressionar `Enter` dentro de um input.
137
+
138
+ ### Exemplo de Implementação
139
+
140
+ ```tsx
141
+ <form
142
+ action="/api/newsletter"
143
+ method="POST"
144
+ // Atributos de Tracking na tag FORM (Obrigatório)
145
+ data-nt-ut-event="newsletter_signup"
146
+ data-nt-ut-category="leads"
147
+ data-nt-ut-label="footer_form"
148
+ // data-nt-ut-type="submit" -> (Opcional: o padrão já é 'submit' para formulários)
149
+ >
150
+ <div className="flex gap-2">
151
+ <input
152
+ type="email"
153
+ name="email"
154
+ placeholder="Seu melhor e-mail"
155
+ className="border p-2"
156
+ />
157
+ <button type="submit" className="bg-blue-500 text-white p-2">
158
+ Inscrever-se
159
+ </button>
160
+ </div>
161
+ </form>
162
+
163
+ ```
164
+
165
+ ### O que acontece no DataLayer?
166
+
167
+ Quando o usuário envia este formulário, o seguinte objeto é enviado para o GTM:
168
+
169
+ ```javascript
170
+ {
171
+ event: "newsletter_signup", // Valor de data-nt-ut-event
172
+ event_category: "leads", // Valor de data-nt-ut-category
173
+ event_label: "footer_form", // Valor de data-nt-ut-label
174
+ event_type: "submit", // Automático para tags <form>
175
+ interaction_time: "2024-01-20T14:00:00.000Z"
176
+ }
177
+
178
+ ```
179
+
180
+ ---
181
+
182
+ ## ⚙️ Configuração no GTM
183
+
184
+ O pacote envia os dados para `window.dataLayer`.
185
+
186
+ ### Exemplo de Objeto Enviado (Read Confirmation)
187
+
188
+ ```javascript
189
+ {
190
+ event: "article_view",
191
+ event_category: "blog",
192
+ event_label: "como_aprender_react",
193
+ event_type: "read_confirmation",
194
+ interaction_time: "..."
195
+ }
196
+
197
+ ```
198
+
199
+ ### Configuração Recomendada
200
+
201
+ 1. **Variáveis:** Crie variáveis de DataLayer para `event_category`, `event_label` e `event_type`.
202
+ 2. **Trigger:** Use `.*` (Regex) em Evento Personalizado para capturar tudo.
203
+ 3. **Tag GA4:** Mapeie os parâmetros. No GA4, você poderá filtrar eventos onde `type` é igual a `read_confirmation` para medir engajamento real.
204
+
205
+ ---
206
+
207
+ ## License
208
+
209
+ ISC © NineTwo
210
+
211
+ ```
212
+
213
+ ```
package/dist/index.d.mts CHANGED
@@ -13,6 +13,7 @@ interface TrackViewProps {
13
13
  category?: string;
14
14
  label?: string;
15
15
  threshold?: number;
16
+ readTime?: number;
16
17
  }
17
18
  declare const TrackView: React.FC<TrackViewProps>;
18
19
 
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ interface TrackViewProps {
13
13
  category?: string;
14
14
  label?: string;
15
15
  threshold?: number;
16
+ readTime?: number;
16
17
  }
17
18
  declare const TrackView: React.FC<TrackViewProps>;
18
19
 
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ __export(src_exports, {
27
27
  module.exports = __toCommonJS(src_exports);
28
28
 
29
29
  // src/TrackingProvider.tsx
30
- var import_react2 = require("react");
30
+ var import_react3 = require("react");
31
31
 
32
32
  // src/hooks/useAutoTrackClick.ts
33
33
  var import_react = require("react");
@@ -79,6 +79,36 @@ var useAutoTrackClick = (enabled = true) => {
79
79
  }, [enabled]);
80
80
  };
81
81
 
82
+ // src/hooks/useAutoTrackSubmit.ts
83
+ var import_react2 = require("react");
84
+ var useAutoTrackSubmit = (enabled = true) => {
85
+ (0, import_react2.useEffect)(() => {
86
+ if (!enabled || typeof document === "undefined")
87
+ return;
88
+ const handleSubmit = (e) => {
89
+ const target = e.target;
90
+ const formElement = target.closest("form[data-nt-ut-event]");
91
+ if (formElement) {
92
+ const eventName = formElement.getAttribute("data-nt-ut-event");
93
+ const category = formElement.getAttribute("data-nt-ut-category");
94
+ const label = formElement.getAttribute("data-nt-ut-label");
95
+ const type = formElement.getAttribute("data-nt-ut-type");
96
+ pushToDataLayer({
97
+ event: eventName || "form_submit",
98
+ category: category || "form",
99
+ label: label || "",
100
+ type: type || "submit"
101
+ // Padrão agora é 'submit'
102
+ });
103
+ }
104
+ };
105
+ document.addEventListener("submit", handleSubmit, true);
106
+ return () => {
107
+ document.removeEventListener("submit", handleSubmit, true);
108
+ };
109
+ }, [enabled]);
110
+ };
111
+
82
112
  // src/TrackingProvider.tsx
83
113
  var import_jsx_runtime = require("react/jsx-runtime");
84
114
  var TrackingProvider = ({
@@ -87,7 +117,8 @@ var TrackingProvider = ({
87
117
  debug = false
88
118
  }) => {
89
119
  useAutoTrackClick(true);
90
- (0, import_react2.useEffect)(() => {
120
+ useAutoTrackSubmit(true);
121
+ (0, import_react3.useEffect)(() => {
91
122
  if (!gtmId || typeof window === "undefined") {
92
123
  if (debug && !gtmId)
93
124
  console.warn("[NineTwo Tracking] GTM ID n\xE3o fornecido.");
@@ -107,14 +138,14 @@ var TrackingProvider = ({
107
138
  const script = document.createElement("script");
108
139
  script.id = scriptId;
109
140
  script.async = true;
110
- script.src = `https://www.googletagmanager.com/gtag/js?id=${gtmId}`;
141
+ script.src = `https://www.googletagmanager.com/gtm.js?id=${gtmId}`;
111
142
  script.onload = () => {
112
143
  if (debug)
113
144
  console.log(`[NineTwo Tracking] \u2705 GTM carregado com sucesso! (${gtmId})`);
114
145
  };
115
146
  script.onerror = () => {
116
147
  if (debug)
117
- console.error("[NineTwo Tracking] \u274C Erro ao carregar GTM. Verifique AdBlockers.");
148
+ console.error("[NineTwo Tracking] \u274C Erro ao carregar GTM.");
118
149
  };
119
150
  document.head.appendChild(script);
120
151
  }, [gtmId, debug]);
@@ -122,37 +153,66 @@ var TrackingProvider = ({
122
153
  };
123
154
 
124
155
  // src/components/TrackView.tsx
125
- var import_react3 = require("react");
156
+ var import_react4 = require("react");
126
157
  var import_jsx_runtime2 = require("react/jsx-runtime");
127
158
  var TrackView = ({
128
159
  children,
129
160
  eventName,
130
161
  category,
131
162
  label,
132
- threshold = 0.5
163
+ threshold = 0.5,
164
+ readTime = 5e3
133
165
  }) => {
134
- const ref = (0, import_react3.useRef)(null);
135
- const [hasTriggered, setHasTriggered] = (0, import_react3.useState)(false);
136
- (0, import_react3.useEffect)(() => {
166
+ const ref = (0, import_react4.useRef)(null);
167
+ const timerRef = (0, import_react4.useRef)(null);
168
+ const [hasTriggeredView, setHasTriggeredView] = (0, import_react4.useState)(false);
169
+ const [hasTriggeredRead, setHasTriggeredRead] = (0, import_react4.useState)(false);
170
+ (0, import_react4.useEffect)(() => {
171
+ if (!ref.current)
172
+ return;
173
+ if (hasTriggeredView && hasTriggeredRead)
174
+ return;
137
175
  const observer = new IntersectionObserver(
138
176
  ([entry]) => {
139
- if (entry.isIntersecting && !hasTriggered) {
140
- pushToDataLayer({
141
- event: eventName,
142
- category,
143
- label,
144
- type: "view"
145
- });
146
- setHasTriggered(true);
147
- observer.disconnect();
177
+ if (entry.isIntersecting) {
178
+ if (!hasTriggeredView) {
179
+ pushToDataLayer({
180
+ event: eventName,
181
+ category,
182
+ label,
183
+ type: "view"
184
+ });
185
+ setHasTriggeredView(true);
186
+ }
187
+ if (!hasTriggeredRead && !timerRef.current) {
188
+ timerRef.current = setTimeout(() => {
189
+ pushToDataLayer({
190
+ event: `${eventName}_read_confirmation`,
191
+ // Sufixo solicitado
192
+ category,
193
+ label,
194
+ type: "read_confirmation"
195
+ // Tipo diferenciado
196
+ });
197
+ setHasTriggeredRead(true);
198
+ }, readTime);
199
+ }
200
+ } else {
201
+ if (timerRef.current) {
202
+ clearTimeout(timerRef.current);
203
+ timerRef.current = null;
204
+ }
148
205
  }
149
206
  },
150
207
  { threshold }
151
208
  );
152
- if (ref.current)
153
- observer.observe(ref.current);
154
- return () => observer.disconnect();
155
- }, [hasTriggered, eventName, category, label, threshold]);
209
+ observer.observe(ref.current);
210
+ return () => {
211
+ observer.disconnect();
212
+ if (timerRef.current)
213
+ clearTimeout(timerRef.current);
214
+ };
215
+ }, [hasTriggeredView, hasTriggeredRead, eventName, category, label, threshold, readTime]);
156
216
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref, style: { display: "contents" }, children });
157
217
  };
158
218
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/TrackingProvider.tsx
2
- import { useEffect as useEffect2 } from "react";
2
+ import { useEffect as useEffect3 } from "react";
3
3
 
4
4
  // src/hooks/useAutoTrackClick.ts
5
5
  import { useEffect } from "react";
@@ -51,6 +51,36 @@ var useAutoTrackClick = (enabled = true) => {
51
51
  }, [enabled]);
52
52
  };
53
53
 
54
+ // src/hooks/useAutoTrackSubmit.ts
55
+ import { useEffect as useEffect2 } from "react";
56
+ var useAutoTrackSubmit = (enabled = true) => {
57
+ useEffect2(() => {
58
+ if (!enabled || typeof document === "undefined")
59
+ return;
60
+ const handleSubmit = (e) => {
61
+ const target = e.target;
62
+ const formElement = target.closest("form[data-nt-ut-event]");
63
+ if (formElement) {
64
+ const eventName = formElement.getAttribute("data-nt-ut-event");
65
+ const category = formElement.getAttribute("data-nt-ut-category");
66
+ const label = formElement.getAttribute("data-nt-ut-label");
67
+ const type = formElement.getAttribute("data-nt-ut-type");
68
+ pushToDataLayer({
69
+ event: eventName || "form_submit",
70
+ category: category || "form",
71
+ label: label || "",
72
+ type: type || "submit"
73
+ // Padrão agora é 'submit'
74
+ });
75
+ }
76
+ };
77
+ document.addEventListener("submit", handleSubmit, true);
78
+ return () => {
79
+ document.removeEventListener("submit", handleSubmit, true);
80
+ };
81
+ }, [enabled]);
82
+ };
83
+
54
84
  // src/TrackingProvider.tsx
55
85
  import { Fragment, jsx } from "react/jsx-runtime";
56
86
  var TrackingProvider = ({
@@ -59,7 +89,8 @@ var TrackingProvider = ({
59
89
  debug = false
60
90
  }) => {
61
91
  useAutoTrackClick(true);
62
- useEffect2(() => {
92
+ useAutoTrackSubmit(true);
93
+ useEffect3(() => {
63
94
  if (!gtmId || typeof window === "undefined") {
64
95
  if (debug && !gtmId)
65
96
  console.warn("[NineTwo Tracking] GTM ID n\xE3o fornecido.");
@@ -79,14 +110,14 @@ var TrackingProvider = ({
79
110
  const script = document.createElement("script");
80
111
  script.id = scriptId;
81
112
  script.async = true;
82
- script.src = `https://www.googletagmanager.com/gtag/js?id=${gtmId}`;
113
+ script.src = `https://www.googletagmanager.com/gtm.js?id=${gtmId}`;
83
114
  script.onload = () => {
84
115
  if (debug)
85
116
  console.log(`[NineTwo Tracking] \u2705 GTM carregado com sucesso! (${gtmId})`);
86
117
  };
87
118
  script.onerror = () => {
88
119
  if (debug)
89
- console.error("[NineTwo Tracking] \u274C Erro ao carregar GTM. Verifique AdBlockers.");
120
+ console.error("[NineTwo Tracking] \u274C Erro ao carregar GTM.");
90
121
  };
91
122
  document.head.appendChild(script);
92
123
  }, [gtmId, debug]);
@@ -94,37 +125,66 @@ var TrackingProvider = ({
94
125
  };
95
126
 
96
127
  // src/components/TrackView.tsx
97
- import { useEffect as useEffect3, useRef, useState } from "react";
128
+ import { useEffect as useEffect4, useRef, useState } from "react";
98
129
  import { jsx as jsx2 } from "react/jsx-runtime";
99
130
  var TrackView = ({
100
131
  children,
101
132
  eventName,
102
133
  category,
103
134
  label,
104
- threshold = 0.5
135
+ threshold = 0.5,
136
+ readTime = 5e3
105
137
  }) => {
106
138
  const ref = useRef(null);
107
- const [hasTriggered, setHasTriggered] = useState(false);
108
- useEffect3(() => {
139
+ const timerRef = useRef(null);
140
+ const [hasTriggeredView, setHasTriggeredView] = useState(false);
141
+ const [hasTriggeredRead, setHasTriggeredRead] = useState(false);
142
+ useEffect4(() => {
143
+ if (!ref.current)
144
+ return;
145
+ if (hasTriggeredView && hasTriggeredRead)
146
+ return;
109
147
  const observer = new IntersectionObserver(
110
148
  ([entry]) => {
111
- if (entry.isIntersecting && !hasTriggered) {
112
- pushToDataLayer({
113
- event: eventName,
114
- category,
115
- label,
116
- type: "view"
117
- });
118
- setHasTriggered(true);
119
- observer.disconnect();
149
+ if (entry.isIntersecting) {
150
+ if (!hasTriggeredView) {
151
+ pushToDataLayer({
152
+ event: eventName,
153
+ category,
154
+ label,
155
+ type: "view"
156
+ });
157
+ setHasTriggeredView(true);
158
+ }
159
+ if (!hasTriggeredRead && !timerRef.current) {
160
+ timerRef.current = setTimeout(() => {
161
+ pushToDataLayer({
162
+ event: `${eventName}_read_confirmation`,
163
+ // Sufixo solicitado
164
+ category,
165
+ label,
166
+ type: "read_confirmation"
167
+ // Tipo diferenciado
168
+ });
169
+ setHasTriggeredRead(true);
170
+ }, readTime);
171
+ }
172
+ } else {
173
+ if (timerRef.current) {
174
+ clearTimeout(timerRef.current);
175
+ timerRef.current = null;
176
+ }
120
177
  }
121
178
  },
122
179
  { threshold }
123
180
  );
124
- if (ref.current)
125
- observer.observe(ref.current);
126
- return () => observer.disconnect();
127
- }, [hasTriggered, eventName, category, label, threshold]);
181
+ observer.observe(ref.current);
182
+ return () => {
183
+ observer.disconnect();
184
+ if (timerRef.current)
185
+ clearTimeout(timerRef.current);
186
+ };
187
+ }, [hasTriggeredView, hasTriggeredRead, eventName, category, label, threshold, readTime]);
128
188
  return /* @__PURE__ */ jsx2("div", { ref, style: { display: "contents" }, children });
129
189
  };
130
190
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ninetwo-user-tracking",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "User tracking abstraction for React/Nextjs with GTM",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",