amplitude-auto-track 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/bin/generate-events.mjs +163 -0
- package/dist/index.d.mts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +202 -0
- package/dist/index.mjs +178 -0
- package/package.json +48 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* amplitude-auto-track generate
|
|
4
|
+
*
|
|
5
|
+
* Scans your app for Korean button/link text and generates event-names.json
|
|
6
|
+
* using OpenAI. Existing entries are never overwritten.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx amplitude-auto-track # scans ./app or ./src
|
|
10
|
+
* npx amplitude-auto-track --dir src # custom scan directory
|
|
11
|
+
* npx amplitude-auto-track --out lib/events.json # custom output path
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import dotenv from "dotenv";
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import OpenAI from "openai";
|
|
18
|
+
|
|
19
|
+
dotenv.config();
|
|
20
|
+
|
|
21
|
+
const ROOT = process.cwd();
|
|
22
|
+
|
|
23
|
+
// ── CLI args ────────────────────────────────────────────────────────────────
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
const getArg = (flag) => {
|
|
26
|
+
const idx = args.indexOf(flag);
|
|
27
|
+
return idx !== -1 ? args[idx + 1] : null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const dirArg = getArg("--dir");
|
|
31
|
+
const outArg = getArg("--out");
|
|
32
|
+
|
|
33
|
+
const SCAN_DIR = dirArg
|
|
34
|
+
? path.join(ROOT, dirArg)
|
|
35
|
+
: fs.existsSync(path.join(ROOT, "app"))
|
|
36
|
+
? path.join(ROOT, "app")
|
|
37
|
+
: path.join(ROOT, "src");
|
|
38
|
+
|
|
39
|
+
const OUT_PATH = outArg
|
|
40
|
+
? path.join(ROOT, outArg)
|
|
41
|
+
: path.join(ROOT, "lib", "event-names.json");
|
|
42
|
+
|
|
43
|
+
// ── 1. Load existing mappings ───────────────────────────────────────────────
|
|
44
|
+
fs.mkdirSync(path.dirname(OUT_PATH), { recursive: true });
|
|
45
|
+
const existing = fs.existsSync(OUT_PATH)
|
|
46
|
+
? JSON.parse(fs.readFileSync(OUT_PATH, "utf-8"))
|
|
47
|
+
: {};
|
|
48
|
+
|
|
49
|
+
// ── 2. Scan for Korean clickable text ───────────────────────────────────────
|
|
50
|
+
const hasKorean = /[\uAC00-\uD7AF]/;
|
|
51
|
+
|
|
52
|
+
function extractKoreanFromInner(inner) {
|
|
53
|
+
const results = new Set();
|
|
54
|
+
const trimmed = inner.trim();
|
|
55
|
+
if (trimmed && hasKorean.test(trimmed) && !trimmed.includes("<")) {
|
|
56
|
+
results.add(trimmed);
|
|
57
|
+
}
|
|
58
|
+
const textNode = />([^<{}]*[\uAC00-\uD7AF][^<{}]*)</g;
|
|
59
|
+
let m;
|
|
60
|
+
while ((m = textNode.exec(inner)) !== null) {
|
|
61
|
+
const t = m[1].trim();
|
|
62
|
+
if (t && hasKorean.test(t)) results.add(t);
|
|
63
|
+
}
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractClickableKorean(content) {
|
|
68
|
+
const results = new Set();
|
|
69
|
+
let m;
|
|
70
|
+
const buttonRe = /<button[\s>][^>]*>([\s\S]*?)<\/button>/gi;
|
|
71
|
+
while ((m = buttonRe.exec(content)) !== null)
|
|
72
|
+
for (const t of extractKoreanFromInner(m[1])) results.add(t);
|
|
73
|
+
|
|
74
|
+
const anchorRe = /<a\s[^>]*>([\s\S]*?)<\/a>/gi;
|
|
75
|
+
while ((m = anchorRe.exec(content)) !== null)
|
|
76
|
+
for (const t of extractKoreanFromInner(m[1])) results.add(t);
|
|
77
|
+
|
|
78
|
+
const propRe =
|
|
79
|
+
/(?:buttonText|label|aria-label|title)\s*=\s*\{?["'`]([^"'`\n]*[\uAC00-\uD7AF][^"'`\n]*)["'`]/g;
|
|
80
|
+
while ((m = propRe.exec(content)) !== null) {
|
|
81
|
+
const t = m[1].trim();
|
|
82
|
+
if (t) results.add(t);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const dataRe =
|
|
86
|
+
/(?:buttonText|label)\s*:\s*["'`]([^"'`\n]*[\uAC00-\uD7AF][^"'`\n]*)["'`]/g;
|
|
87
|
+
while ((m = dataRe.exec(content)) !== null) {
|
|
88
|
+
const t = m[1].trim();
|
|
89
|
+
if (t) results.add(t);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function scanDir(dir) {
|
|
96
|
+
const all = new Set();
|
|
97
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
98
|
+
if (entry.name === "node_modules") continue;
|
|
99
|
+
const full = path.join(dir, entry.name);
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
for (const t of scanDir(full)) all.add(t);
|
|
102
|
+
} else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
|
|
103
|
+
for (const t of extractClickableKorean(fs.readFileSync(full, "utf-8")))
|
|
104
|
+
all.add(t);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return all;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!fs.existsSync(SCAN_DIR)) {
|
|
111
|
+
console.error(`Error: scan directory not found: ${SCAN_DIR}`);
|
|
112
|
+
console.error(`Use --dir to specify a directory, e.g. --dir src`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const allTexts = scanDir(SCAN_DIR);
|
|
117
|
+
const toTranslate = [...allTexts].filter((t) => !existing[t]);
|
|
118
|
+
|
|
119
|
+
if (toTranslate.length === 0) {
|
|
120
|
+
console.log("✓ No new texts found. event-names.json is up to date.");
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(`Found ${toTranslate.length} new text(s):`, toTranslate);
|
|
125
|
+
|
|
126
|
+
// ── 3. Translate via OpenAI ─────────────────────────────────────────────────
|
|
127
|
+
const apiKey = process.env.OPENAI_API_KEY || process.env.NEXT_PUBLIC_OPENAI_API_KEY;
|
|
128
|
+
if (!apiKey) {
|
|
129
|
+
console.error("Error: OPENAI_API_KEY is not set in your .env file.");
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const client = new OpenAI({ apiKey });
|
|
134
|
+
const response = await client.chat.completions.create({
|
|
135
|
+
model: "gpt-4o-mini",
|
|
136
|
+
messages: [
|
|
137
|
+
{
|
|
138
|
+
role: "system",
|
|
139
|
+
content: `Convert button/link text into snake_case analytics event names.
|
|
140
|
+
Rules:
|
|
141
|
+
- Convert any language to English
|
|
142
|
+
- Use lowercase snake_case
|
|
143
|
+
- Keep it concise (max 3-4 words)
|
|
144
|
+
- Add "_clicked" suffix
|
|
145
|
+
- Return ONLY a JSON object mapping each input to its event name`,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
role: "user",
|
|
149
|
+
content: `Convert these texts:\n${JSON.stringify(toTranslate)}`,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
temperature: 0.3,
|
|
153
|
+
response_format: { type: "json_object" },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const translations = JSON.parse(response.choices[0].message.content);
|
|
157
|
+
const merged = { ...translations, ...existing };
|
|
158
|
+
const sorted = Object.fromEntries(
|
|
159
|
+
Object.entries(merged).sort(([a], [b]) => a.localeCompare(b))
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
fs.writeFileSync(OUT_PATH, JSON.stringify(sorted, null, 2) + "\n");
|
|
163
|
+
console.log(`✓ Wrote ${Object.keys(sorted).length} entries to ${path.relative(ROOT, OUT_PATH)}`);
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
interface InitAmplitudeOptions {
|
|
2
|
+
/** Event name map generated by `npx amplitude-auto-track`. */
|
|
3
|
+
eventNames?: Record<string, string>;
|
|
4
|
+
/** Amplitude API key. Falls back to NEXT_PUBLIC_AMPLITUDE_API_KEY. */
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
}
|
|
7
|
+
interface AmplitudeTrackDetail {
|
|
8
|
+
name: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
props: Record<string, string | number>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** CustomEvent name fired on window after every amplitude.track() call. */
|
|
14
|
+
declare const AMPLITUDE_TRACK_EVENT: "amplitude:track";
|
|
15
|
+
/**
|
|
16
|
+
* Initialize Amplitude and start auto-tracking all button/link clicks.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* import { initAmplitude } from 'amplitude-auto-track'
|
|
20
|
+
* import eventNames from './lib/event-names.json'
|
|
21
|
+
*
|
|
22
|
+
* initAmplitude({ eventNames })
|
|
23
|
+
*/
|
|
24
|
+
declare function initAmplitude(options?: InitAmplitudeOptions): void;
|
|
25
|
+
/**
|
|
26
|
+
* Send an event to Amplitude directly and emit an `amplitude:track` window event.
|
|
27
|
+
*/
|
|
28
|
+
declare function handleTrackEvent(logName: string, displayName: string, customFields?: Record<string, string | number>): void;
|
|
29
|
+
|
|
30
|
+
export { AMPLITUDE_TRACK_EVENT, type AmplitudeTrackDetail, type InitAmplitudeOptions, handleTrackEvent, initAmplitude };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
interface InitAmplitudeOptions {
|
|
2
|
+
/** Event name map generated by `npx amplitude-auto-track`. */
|
|
3
|
+
eventNames?: Record<string, string>;
|
|
4
|
+
/** Amplitude API key. Falls back to NEXT_PUBLIC_AMPLITUDE_API_KEY. */
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
}
|
|
7
|
+
interface AmplitudeTrackDetail {
|
|
8
|
+
name: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
props: Record<string, string | number>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** CustomEvent name fired on window after every amplitude.track() call. */
|
|
14
|
+
declare const AMPLITUDE_TRACK_EVENT: "amplitude:track";
|
|
15
|
+
/**
|
|
16
|
+
* Initialize Amplitude and start auto-tracking all button/link clicks.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* import { initAmplitude } from 'amplitude-auto-track'
|
|
20
|
+
* import eventNames from './lib/event-names.json'
|
|
21
|
+
*
|
|
22
|
+
* initAmplitude({ eventNames })
|
|
23
|
+
*/
|
|
24
|
+
declare function initAmplitude(options?: InitAmplitudeOptions): void;
|
|
25
|
+
/**
|
|
26
|
+
* Send an event to Amplitude directly and emit an `amplitude:track` window event.
|
|
27
|
+
*/
|
|
28
|
+
declare function handleTrackEvent(logName: string, displayName: string, customFields?: Record<string, string | number>): void;
|
|
29
|
+
|
|
30
|
+
export { AMPLITUDE_TRACK_EVENT, type AmplitudeTrackDetail, type InitAmplitudeOptions, handleTrackEvent, initAmplitude };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var amplitude = require('@amplitude/analytics-browser');
|
|
4
|
+
|
|
5
|
+
function _interopNamespace(e) {
|
|
6
|
+
if (e && e.__esModule) return e;
|
|
7
|
+
var n = Object.create(null);
|
|
8
|
+
if (e) {
|
|
9
|
+
Object.keys(e).forEach(function (k) {
|
|
10
|
+
if (k !== 'default') {
|
|
11
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
12
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () { return e[k]; }
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
n.default = e;
|
|
20
|
+
return Object.freeze(n);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var amplitude__namespace = /*#__PURE__*/_interopNamespace(amplitude);
|
|
24
|
+
|
|
25
|
+
var __defProp = Object.defineProperty;
|
|
26
|
+
var __defProps = Object.defineProperties;
|
|
27
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
28
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
29
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
30
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
31
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
32
|
+
var __spreadValues = (a, b) => {
|
|
33
|
+
for (var prop in b || (b = {}))
|
|
34
|
+
if (__hasOwnProp.call(b, prop))
|
|
35
|
+
__defNormalProp(a, prop, b[prop]);
|
|
36
|
+
if (__getOwnPropSymbols)
|
|
37
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
38
|
+
if (__propIsEnum.call(b, prop))
|
|
39
|
+
__defNormalProp(a, prop, b[prop]);
|
|
40
|
+
}
|
|
41
|
+
return a;
|
|
42
|
+
};
|
|
43
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
44
|
+
var AMPLITUDE_TRACK_EVENT = "amplitude:track";
|
|
45
|
+
var eventMap = {};
|
|
46
|
+
function getEventName(text) {
|
|
47
|
+
var _a;
|
|
48
|
+
if (!text) return "button_clicked";
|
|
49
|
+
return (_a = eventMap[text.trim()]) != null ? _a : "button_clicked";
|
|
50
|
+
}
|
|
51
|
+
function inferLocationFromDOM(el) {
|
|
52
|
+
let current = el.parentElement;
|
|
53
|
+
while (current && current !== document.body) {
|
|
54
|
+
const tag = current.tagName.toLowerCase();
|
|
55
|
+
if (tag === "nav") return current.id || "navbar";
|
|
56
|
+
if (tag === "header") return current.id || "header";
|
|
57
|
+
if (tag === "footer") return current.id || "footer";
|
|
58
|
+
if (tag === "main") return current.id || "main";
|
|
59
|
+
if (tag === "aside") return current.id || "aside";
|
|
60
|
+
if (tag === "section") {
|
|
61
|
+
if (current.id) return current.id;
|
|
62
|
+
const sections = Array.from(document.querySelectorAll("section"));
|
|
63
|
+
const idx = sections.indexOf(current);
|
|
64
|
+
return idx >= 0 ? `section_${idx + 1}` : "section";
|
|
65
|
+
}
|
|
66
|
+
current = current.parentElement;
|
|
67
|
+
}
|
|
68
|
+
return "page";
|
|
69
|
+
}
|
|
70
|
+
function inferContext(el, tagName) {
|
|
71
|
+
var _a, _b;
|
|
72
|
+
const location = el.dataset.location || inferLocationFromDOM(el) || "page";
|
|
73
|
+
let section = (_a = el.dataset.section) != null ? _a : "";
|
|
74
|
+
if (!section) {
|
|
75
|
+
const sectionEl = el.closest("section[id]");
|
|
76
|
+
if (sectionEl) section = sectionEl.id;
|
|
77
|
+
else if (el.closest("nav") || el.closest("header")) section = "header";
|
|
78
|
+
else section = "main";
|
|
79
|
+
}
|
|
80
|
+
const buttonType = (_b = el.dataset.buttonType) != null ? _b : tagName === "A" ? "link" : "button";
|
|
81
|
+
return { buttonType, location, section };
|
|
82
|
+
}
|
|
83
|
+
function inferLocationLabel(el) {
|
|
84
|
+
let current = el.parentElement;
|
|
85
|
+
while (current && current !== document.body) {
|
|
86
|
+
const tag = current.tagName.toLowerCase();
|
|
87
|
+
if (tag === "nav") return current.id || "nav";
|
|
88
|
+
if (tag === "header") return current.id || "header";
|
|
89
|
+
if (tag === "footer") return current.id || "footer";
|
|
90
|
+
if (tag === "main") return current.id || "main";
|
|
91
|
+
if (tag === "aside") return current.id || "aside";
|
|
92
|
+
if (tag === "section") {
|
|
93
|
+
if (current.id) return current.id;
|
|
94
|
+
const sections = Array.from(document.querySelectorAll("section"));
|
|
95
|
+
const idx = sections.indexOf(current);
|
|
96
|
+
return idx >= 0 ? `section_${idx + 1}` : "section";
|
|
97
|
+
}
|
|
98
|
+
current = current.parentElement;
|
|
99
|
+
}
|
|
100
|
+
return "page";
|
|
101
|
+
}
|
|
102
|
+
function autoTagDuplicateElements() {
|
|
103
|
+
var _a;
|
|
104
|
+
const elements = Array.from(
|
|
105
|
+
document.querySelectorAll("button, a")
|
|
106
|
+
);
|
|
107
|
+
const groups = /* @__PURE__ */ new Map();
|
|
108
|
+
for (const el of elements) {
|
|
109
|
+
const text = (_a = el.textContent) == null ? void 0 : _a.trim();
|
|
110
|
+
if (!text) continue;
|
|
111
|
+
if (!groups.has(text)) groups.set(text, []);
|
|
112
|
+
groups.get(text).push(el);
|
|
113
|
+
}
|
|
114
|
+
for (const [, els] of groups) {
|
|
115
|
+
if (els.length <= 1) continue;
|
|
116
|
+
for (const el of els) {
|
|
117
|
+
if (!el.dataset.location) {
|
|
118
|
+
el.dataset.location = inferLocationLabel(el);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
var trackingInitialized = false;
|
|
124
|
+
var debounceTimer = null;
|
|
125
|
+
function scheduleAutoTag() {
|
|
126
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
127
|
+
debounceTimer = setTimeout(() => {
|
|
128
|
+
autoTagDuplicateElements();
|
|
129
|
+
debounceTimer = null;
|
|
130
|
+
}, 200);
|
|
131
|
+
}
|
|
132
|
+
function initAmplitude(options) {
|
|
133
|
+
var _a, _b;
|
|
134
|
+
if (options == null ? void 0 : options.eventNames) eventMap = options.eventNames;
|
|
135
|
+
const apiKey = (_b = options == null ? void 0 : options.apiKey) != null ? _b : typeof process !== "undefined" ? (_a = process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY) != null ? _a : "" : "";
|
|
136
|
+
amplitude__namespace.init(apiKey, void 0, {
|
|
137
|
+
defaultTracking: {
|
|
138
|
+
sessions: true,
|
|
139
|
+
pageViews: true,
|
|
140
|
+
formInteractions: true,
|
|
141
|
+
fileDownloads: true
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
if (typeof window === "undefined") return;
|
|
145
|
+
if (trackingInitialized) return;
|
|
146
|
+
trackingInitialized = true;
|
|
147
|
+
if (document.readyState === "loading") {
|
|
148
|
+
document.addEventListener("DOMContentLoaded", autoTagDuplicateElements);
|
|
149
|
+
} else {
|
|
150
|
+
autoTagDuplicateElements();
|
|
151
|
+
}
|
|
152
|
+
const observer = new MutationObserver(scheduleAutoTag);
|
|
153
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
154
|
+
document.addEventListener("click", (event) => {
|
|
155
|
+
var _a2, _b2;
|
|
156
|
+
const target = event.target;
|
|
157
|
+
const tagName = target.tagName;
|
|
158
|
+
const ctx = inferContext(target, tagName);
|
|
159
|
+
if (tagName === "BUTTON") {
|
|
160
|
+
const text = ((_a2 = target.textContent) == null ? void 0 : _a2.trim()) || "";
|
|
161
|
+
handleTrackEvent(getEventName(text), text, {
|
|
162
|
+
button_text: text,
|
|
163
|
+
button_class: target.className || "",
|
|
164
|
+
element_id: target.id || "",
|
|
165
|
+
element_type: "button",
|
|
166
|
+
text_length: text.length,
|
|
167
|
+
button_type: ctx.buttonType,
|
|
168
|
+
location: ctx.location,
|
|
169
|
+
section: ctx.section
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (tagName === "A") {
|
|
173
|
+
const text = ((_b2 = target.textContent) == null ? void 0 : _b2.trim()) || "";
|
|
174
|
+
handleTrackEvent(getEventName(text), text, {
|
|
175
|
+
link_text: text,
|
|
176
|
+
link_href: target.href || "",
|
|
177
|
+
element_id: target.id || "",
|
|
178
|
+
element_type: "link",
|
|
179
|
+
text_length: text.length,
|
|
180
|
+
button_type: ctx.buttonType,
|
|
181
|
+
location: ctx.location,
|
|
182
|
+
section: ctx.section
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function handleTrackEvent(logName, displayName, customFields = {}) {
|
|
188
|
+
amplitude__namespace.track(logName, __spreadProps(__spreadValues({}, customFields), {
|
|
189
|
+
event_display_name: String(displayName || "")
|
|
190
|
+
}));
|
|
191
|
+
if (typeof window !== "undefined") {
|
|
192
|
+
window.dispatchEvent(
|
|
193
|
+
new CustomEvent(AMPLITUDE_TRACK_EVENT, {
|
|
194
|
+
detail: { name: logName, displayName, props: customFields }
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
exports.AMPLITUDE_TRACK_EVENT = AMPLITUDE_TRACK_EVENT;
|
|
201
|
+
exports.handleTrackEvent = handleTrackEvent;
|
|
202
|
+
exports.initAmplitude = initAmplitude;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import * as amplitude from '@amplitude/analytics-browser';
|
|
2
|
+
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __defProps = Object.defineProperties;
|
|
5
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
6
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
9
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
10
|
+
var __spreadValues = (a, b) => {
|
|
11
|
+
for (var prop in b || (b = {}))
|
|
12
|
+
if (__hasOwnProp.call(b, prop))
|
|
13
|
+
__defNormalProp(a, prop, b[prop]);
|
|
14
|
+
if (__getOwnPropSymbols)
|
|
15
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
16
|
+
if (__propIsEnum.call(b, prop))
|
|
17
|
+
__defNormalProp(a, prop, b[prop]);
|
|
18
|
+
}
|
|
19
|
+
return a;
|
|
20
|
+
};
|
|
21
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
22
|
+
var AMPLITUDE_TRACK_EVENT = "amplitude:track";
|
|
23
|
+
var eventMap = {};
|
|
24
|
+
function getEventName(text) {
|
|
25
|
+
var _a;
|
|
26
|
+
if (!text) return "button_clicked";
|
|
27
|
+
return (_a = eventMap[text.trim()]) != null ? _a : "button_clicked";
|
|
28
|
+
}
|
|
29
|
+
function inferLocationFromDOM(el) {
|
|
30
|
+
let current = el.parentElement;
|
|
31
|
+
while (current && current !== document.body) {
|
|
32
|
+
const tag = current.tagName.toLowerCase();
|
|
33
|
+
if (tag === "nav") return current.id || "navbar";
|
|
34
|
+
if (tag === "header") return current.id || "header";
|
|
35
|
+
if (tag === "footer") return current.id || "footer";
|
|
36
|
+
if (tag === "main") return current.id || "main";
|
|
37
|
+
if (tag === "aside") return current.id || "aside";
|
|
38
|
+
if (tag === "section") {
|
|
39
|
+
if (current.id) return current.id;
|
|
40
|
+
const sections = Array.from(document.querySelectorAll("section"));
|
|
41
|
+
const idx = sections.indexOf(current);
|
|
42
|
+
return idx >= 0 ? `section_${idx + 1}` : "section";
|
|
43
|
+
}
|
|
44
|
+
current = current.parentElement;
|
|
45
|
+
}
|
|
46
|
+
return "page";
|
|
47
|
+
}
|
|
48
|
+
function inferContext(el, tagName) {
|
|
49
|
+
var _a, _b;
|
|
50
|
+
const location = el.dataset.location || inferLocationFromDOM(el) || "page";
|
|
51
|
+
let section = (_a = el.dataset.section) != null ? _a : "";
|
|
52
|
+
if (!section) {
|
|
53
|
+
const sectionEl = el.closest("section[id]");
|
|
54
|
+
if (sectionEl) section = sectionEl.id;
|
|
55
|
+
else if (el.closest("nav") || el.closest("header")) section = "header";
|
|
56
|
+
else section = "main";
|
|
57
|
+
}
|
|
58
|
+
const buttonType = (_b = el.dataset.buttonType) != null ? _b : tagName === "A" ? "link" : "button";
|
|
59
|
+
return { buttonType, location, section };
|
|
60
|
+
}
|
|
61
|
+
function inferLocationLabel(el) {
|
|
62
|
+
let current = el.parentElement;
|
|
63
|
+
while (current && current !== document.body) {
|
|
64
|
+
const tag = current.tagName.toLowerCase();
|
|
65
|
+
if (tag === "nav") return current.id || "nav";
|
|
66
|
+
if (tag === "header") return current.id || "header";
|
|
67
|
+
if (tag === "footer") return current.id || "footer";
|
|
68
|
+
if (tag === "main") return current.id || "main";
|
|
69
|
+
if (tag === "aside") return current.id || "aside";
|
|
70
|
+
if (tag === "section") {
|
|
71
|
+
if (current.id) return current.id;
|
|
72
|
+
const sections = Array.from(document.querySelectorAll("section"));
|
|
73
|
+
const idx = sections.indexOf(current);
|
|
74
|
+
return idx >= 0 ? `section_${idx + 1}` : "section";
|
|
75
|
+
}
|
|
76
|
+
current = current.parentElement;
|
|
77
|
+
}
|
|
78
|
+
return "page";
|
|
79
|
+
}
|
|
80
|
+
function autoTagDuplicateElements() {
|
|
81
|
+
var _a;
|
|
82
|
+
const elements = Array.from(
|
|
83
|
+
document.querySelectorAll("button, a")
|
|
84
|
+
);
|
|
85
|
+
const groups = /* @__PURE__ */ new Map();
|
|
86
|
+
for (const el of elements) {
|
|
87
|
+
const text = (_a = el.textContent) == null ? void 0 : _a.trim();
|
|
88
|
+
if (!text) continue;
|
|
89
|
+
if (!groups.has(text)) groups.set(text, []);
|
|
90
|
+
groups.get(text).push(el);
|
|
91
|
+
}
|
|
92
|
+
for (const [, els] of groups) {
|
|
93
|
+
if (els.length <= 1) continue;
|
|
94
|
+
for (const el of els) {
|
|
95
|
+
if (!el.dataset.location) {
|
|
96
|
+
el.dataset.location = inferLocationLabel(el);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
var trackingInitialized = false;
|
|
102
|
+
var debounceTimer = null;
|
|
103
|
+
function scheduleAutoTag() {
|
|
104
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
105
|
+
debounceTimer = setTimeout(() => {
|
|
106
|
+
autoTagDuplicateElements();
|
|
107
|
+
debounceTimer = null;
|
|
108
|
+
}, 200);
|
|
109
|
+
}
|
|
110
|
+
function initAmplitude(options) {
|
|
111
|
+
var _a, _b;
|
|
112
|
+
if (options == null ? void 0 : options.eventNames) eventMap = options.eventNames;
|
|
113
|
+
const apiKey = (_b = options == null ? void 0 : options.apiKey) != null ? _b : typeof process !== "undefined" ? (_a = process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY) != null ? _a : "" : "";
|
|
114
|
+
amplitude.init(apiKey, void 0, {
|
|
115
|
+
defaultTracking: {
|
|
116
|
+
sessions: true,
|
|
117
|
+
pageViews: true,
|
|
118
|
+
formInteractions: true,
|
|
119
|
+
fileDownloads: true
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
if (typeof window === "undefined") return;
|
|
123
|
+
if (trackingInitialized) return;
|
|
124
|
+
trackingInitialized = true;
|
|
125
|
+
if (document.readyState === "loading") {
|
|
126
|
+
document.addEventListener("DOMContentLoaded", autoTagDuplicateElements);
|
|
127
|
+
} else {
|
|
128
|
+
autoTagDuplicateElements();
|
|
129
|
+
}
|
|
130
|
+
const observer = new MutationObserver(scheduleAutoTag);
|
|
131
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
132
|
+
document.addEventListener("click", (event) => {
|
|
133
|
+
var _a2, _b2;
|
|
134
|
+
const target = event.target;
|
|
135
|
+
const tagName = target.tagName;
|
|
136
|
+
const ctx = inferContext(target, tagName);
|
|
137
|
+
if (tagName === "BUTTON") {
|
|
138
|
+
const text = ((_a2 = target.textContent) == null ? void 0 : _a2.trim()) || "";
|
|
139
|
+
handleTrackEvent(getEventName(text), text, {
|
|
140
|
+
button_text: text,
|
|
141
|
+
button_class: target.className || "",
|
|
142
|
+
element_id: target.id || "",
|
|
143
|
+
element_type: "button",
|
|
144
|
+
text_length: text.length,
|
|
145
|
+
button_type: ctx.buttonType,
|
|
146
|
+
location: ctx.location,
|
|
147
|
+
section: ctx.section
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (tagName === "A") {
|
|
151
|
+
const text = ((_b2 = target.textContent) == null ? void 0 : _b2.trim()) || "";
|
|
152
|
+
handleTrackEvent(getEventName(text), text, {
|
|
153
|
+
link_text: text,
|
|
154
|
+
link_href: target.href || "",
|
|
155
|
+
element_id: target.id || "",
|
|
156
|
+
element_type: "link",
|
|
157
|
+
text_length: text.length,
|
|
158
|
+
button_type: ctx.buttonType,
|
|
159
|
+
location: ctx.location,
|
|
160
|
+
section: ctx.section
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function handleTrackEvent(logName, displayName, customFields = {}) {
|
|
166
|
+
amplitude.track(logName, __spreadProps(__spreadValues({}, customFields), {
|
|
167
|
+
event_display_name: String(displayName || "")
|
|
168
|
+
}));
|
|
169
|
+
if (typeof window !== "undefined") {
|
|
170
|
+
window.dispatchEvent(
|
|
171
|
+
new CustomEvent(AMPLITUDE_TRACK_EVENT, {
|
|
172
|
+
detail: { name: logName, displayName, props: customFields }
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export { AMPLITUDE_TRACK_EVENT, handleTrackEvent, initAmplitude };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "amplitude-auto-track",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-config Amplitude auto-tracking for Korean Next.js apps. Build-time event name generation via OpenAI + DOM-inferred location/section.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"amplitude",
|
|
7
|
+
"analytics",
|
|
8
|
+
"next.js",
|
|
9
|
+
"auto-track",
|
|
10
|
+
"korean"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"module": "./dist/index.mjs",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.mjs",
|
|
20
|
+
"require": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"amplitude-auto-track": "./bin/generate-events.mjs"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"bin"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup",
|
|
32
|
+
"dev": "tsup --watch",
|
|
33
|
+
"prepublishOnly": "npm run build"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@amplitude/analytics-browser": "^2.36.5",
|
|
37
|
+
"next": ">=13"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^20",
|
|
41
|
+
"tsup": "^8",
|
|
42
|
+
"typescript": "^5"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"dotenv": "^17.3.1",
|
|
46
|
+
"openai": "^6.29.0"
|
|
47
|
+
}
|
|
48
|
+
}
|