@datalyr/wizard 1.0.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 +95 -0
- package/dist/bin/wizard.js +2580 -0
- package/dist/bin/wizard.js.map +1 -0
- package/dist/index.d.mts +64 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +1312 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1275 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,2580 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
"use strict";
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
// bin/wizard.ts
|
|
28
|
+
var import_yargs = __toESM(require("yargs"));
|
|
29
|
+
var import_helpers = require("yargs/helpers");
|
|
30
|
+
|
|
31
|
+
// src/agent/runner.ts
|
|
32
|
+
var p = __toESM(require("@clack/prompts"));
|
|
33
|
+
var import_chalk = __toESM(require("chalk"));
|
|
34
|
+
|
|
35
|
+
// src/agent/interface.ts
|
|
36
|
+
var AGENT_SIGNALS = {
|
|
37
|
+
STATUS: "[STATUS]",
|
|
38
|
+
ERROR_MISSING_KEY: "[ERROR_MISSING_KEY]",
|
|
39
|
+
ERROR_FAILED: "[ERROR_FAILED]",
|
|
40
|
+
SUCCESS: "[SUCCESS]"
|
|
41
|
+
};
|
|
42
|
+
var ALLOWED_COMMANDS = [
|
|
43
|
+
// Package managers
|
|
44
|
+
"npm",
|
|
45
|
+
"yarn",
|
|
46
|
+
"pnpm",
|
|
47
|
+
"bun",
|
|
48
|
+
"npx",
|
|
49
|
+
// Build tools
|
|
50
|
+
"tsc",
|
|
51
|
+
"node",
|
|
52
|
+
// iOS
|
|
53
|
+
"pod",
|
|
54
|
+
"xcodebuild",
|
|
55
|
+
// File operations (read-only)
|
|
56
|
+
"cat",
|
|
57
|
+
"ls",
|
|
58
|
+
"find",
|
|
59
|
+
"grep",
|
|
60
|
+
"head",
|
|
61
|
+
"tail",
|
|
62
|
+
"wc",
|
|
63
|
+
// Git (read-only)
|
|
64
|
+
"git status",
|
|
65
|
+
"git log",
|
|
66
|
+
"git diff",
|
|
67
|
+
"git branch"
|
|
68
|
+
];
|
|
69
|
+
var BLOCKED_PATTERNS = [
|
|
70
|
+
/;/,
|
|
71
|
+
// Command chaining
|
|
72
|
+
/`/,
|
|
73
|
+
// Backticks
|
|
74
|
+
/\$\(/,
|
|
75
|
+
// Command substitution
|
|
76
|
+
/\|\s*sh/,
|
|
77
|
+
// Piping to shell
|
|
78
|
+
/\|\s*bash/,
|
|
79
|
+
// Piping to bash
|
|
80
|
+
/rm\s+-rf/,
|
|
81
|
+
// Dangerous rm
|
|
82
|
+
/>\s*\//,
|
|
83
|
+
// Overwriting system files
|
|
84
|
+
/&&\s*rm/
|
|
85
|
+
// rm after &&
|
|
86
|
+
];
|
|
87
|
+
function validateBashCommand(command) {
|
|
88
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
89
|
+
if (pattern.test(command)) {
|
|
90
|
+
return { allowed: false, reason: `Blocked pattern detected: ${pattern}` };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const baseCommand = command.trim().split(/\s+/)[0];
|
|
94
|
+
const isAllowed = ALLOWED_COMMANDS.some((allowed) => {
|
|
95
|
+
if (allowed.includes(" ")) {
|
|
96
|
+
return command.startsWith(allowed);
|
|
97
|
+
}
|
|
98
|
+
return baseCommand === allowed;
|
|
99
|
+
});
|
|
100
|
+
if (!isAllowed) {
|
|
101
|
+
return { allowed: false, reason: `Command not in allowlist: ${baseCommand}` };
|
|
102
|
+
}
|
|
103
|
+
return { allowed: true };
|
|
104
|
+
}
|
|
105
|
+
function buildSystemPrompt() {
|
|
106
|
+
return `<role>
|
|
107
|
+
You are a senior developer specializing in SDK integrations. You have 10+ years of experience integrating analytics tools into React, Next.js, Svelte, React Native, and iOS projects. You are meticulous, always read code before modifying it, and preserve existing functionality.
|
|
108
|
+
</role>
|
|
109
|
+
|
|
110
|
+
<task>
|
|
111
|
+
Install and configure the Datalyr analytics SDK in the user's project. Complete this task by:
|
|
112
|
+
1. Detecting the framework and understanding the project structure
|
|
113
|
+
2. Installing the correct SDK packages
|
|
114
|
+
3. Creating initialization code in the appropriate entry point
|
|
115
|
+
4. Configuring environment variables
|
|
116
|
+
</task>
|
|
117
|
+
|
|
118
|
+
<sdks>
|
|
119
|
+
| SDK | Use Case | Install Command |
|
|
120
|
+
|-----|----------|-----------------|
|
|
121
|
+
| @datalyr/web | Browser apps (React, Vue, Svelte, Next.js client) | npm install @datalyr/web |
|
|
122
|
+
| @datalyr/api | Server-side (Next.js API routes, Express, Node) | npm install @datalyr/api |
|
|
123
|
+
| @datalyr/react-native | React Native & Expo mobile apps | npm install @datalyr/react-native |
|
|
124
|
+
| DatalyrSDK | Native iOS Swift apps | Swift Package Manager |
|
|
125
|
+
</sdks>
|
|
126
|
+
|
|
127
|
+
<rules>
|
|
128
|
+
1. ALWAYS read files before modifying them - never edit blind
|
|
129
|
+
2. PRESERVE existing code - only add Datalyr, never remove functionality
|
|
130
|
+
3. MATCH the project's code style (indentation, quotes, semicolons)
|
|
131
|
+
4. USE TypeScript if the project uses TypeScript
|
|
132
|
+
5. PLACE initialization in the correct entry point for the framework
|
|
133
|
+
6. UPDATE .env or .env.local with NEXT_PUBLIC_DATALYR_WORKSPACE_ID
|
|
134
|
+
7. CHECK for existing Datalyr setup first - don't duplicate
|
|
135
|
+
</rules>
|
|
136
|
+
|
|
137
|
+
<workflow>
|
|
138
|
+
Step 1: Read package.json to detect framework and package manager
|
|
139
|
+
Step 2: List files to find entry points (app/layout.tsx, src/main.tsx, App.tsx, etc.)
|
|
140
|
+
Step 3: Read entry point files to understand current structure
|
|
141
|
+
Step 4: Install SDK packages using detected package manager
|
|
142
|
+
Step 5: Create initialization file (lib/datalyr.ts or similar)
|
|
143
|
+
Step 6: Update entry point to import and initialize Datalyr
|
|
144
|
+
Step 7: Update or create .env.local with workspace ID placeholder
|
|
145
|
+
Step 8: Call task_complete with summary of changes
|
|
146
|
+
</workflow>
|
|
147
|
+
|
|
148
|
+
<examples>
|
|
149
|
+
<example name="nextjs-app-router">
|
|
150
|
+
For Next.js 13+ with App Router, create app/providers.tsx:
|
|
151
|
+
\`\`\`tsx
|
|
152
|
+
'use client';
|
|
153
|
+
import datalyr from '@datalyr/web';
|
|
154
|
+
import { useEffect } from 'react';
|
|
155
|
+
|
|
156
|
+
export function DatalyrProvider({ children }: { children: React.ReactNode }) {
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
datalyr.init({
|
|
159
|
+
workspaceId: process.env.NEXT_PUBLIC_DATALYR_WORKSPACE_ID!,
|
|
160
|
+
debug: process.env.NODE_ENV === 'development',
|
|
161
|
+
});
|
|
162
|
+
}, []);
|
|
163
|
+
return <>{children}</>;
|
|
164
|
+
}
|
|
165
|
+
\`\`\`
|
|
166
|
+
Then wrap children in app/layout.tsx with <DatalyrProvider>.
|
|
167
|
+
</example>
|
|
168
|
+
|
|
169
|
+
<example name="react-vite">
|
|
170
|
+
For React + Vite, create src/lib/datalyr.ts:
|
|
171
|
+
\`\`\`ts
|
|
172
|
+
import datalyr from '@datalyr/web';
|
|
173
|
+
|
|
174
|
+
let initialized = false;
|
|
175
|
+
|
|
176
|
+
export function initDatalyr() {
|
|
177
|
+
if (initialized) return;
|
|
178
|
+
datalyr.init({
|
|
179
|
+
workspaceId: import.meta.env.VITE_DATALYR_WORKSPACE_ID,
|
|
180
|
+
debug: import.meta.env.DEV,
|
|
181
|
+
});
|
|
182
|
+
initialized = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export { datalyr };
|
|
186
|
+
\`\`\`
|
|
187
|
+
Then call initDatalyr() at the top of src/main.tsx.
|
|
188
|
+
</example>
|
|
189
|
+
|
|
190
|
+
<example name="react-native">
|
|
191
|
+
For React Native, create src/utils/datalyr.ts:
|
|
192
|
+
\`\`\`ts
|
|
193
|
+
import { Datalyr } from '@datalyr/react-native';
|
|
194
|
+
|
|
195
|
+
export async function initDatalyr(apiKey: string) {
|
|
196
|
+
await Datalyr.initialize({
|
|
197
|
+
apiKey,
|
|
198
|
+
enableAutoEvents: true,
|
|
199
|
+
debug: __DEV__,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export { Datalyr };
|
|
204
|
+
\`\`\`
|
|
205
|
+
Then call initDatalyr() in App.tsx useEffect.
|
|
206
|
+
</example>
|
|
207
|
+
</examples>
|
|
208
|
+
|
|
209
|
+
<signals>
|
|
210
|
+
When complete: ${AGENT_SIGNALS.SUCCESS}
|
|
211
|
+
On error: ${AGENT_SIGNALS.ERROR_FAILED}
|
|
212
|
+
Status updates: ${AGENT_SIGNALS.STATUS} <message>
|
|
213
|
+
</signals>`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/detection/detector.ts
|
|
217
|
+
var import_path2 = __toESM(require("path"));
|
|
218
|
+
|
|
219
|
+
// src/utils/fs.ts
|
|
220
|
+
var import_fs_extra = __toESM(require("fs-extra"));
|
|
221
|
+
var import_path = __toESM(require("path"));
|
|
222
|
+
var import_glob = require("glob");
|
|
223
|
+
async function fileExists(filePath) {
|
|
224
|
+
try {
|
|
225
|
+
await import_fs_extra.default.access(filePath);
|
|
226
|
+
return true;
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function readJson(filePath) {
|
|
232
|
+
try {
|
|
233
|
+
return await import_fs_extra.default.readJson(filePath);
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function readPackageJson(cwd) {
|
|
239
|
+
return readJson(import_path.default.join(cwd, "package.json"));
|
|
240
|
+
}
|
|
241
|
+
async function findFile(cwd, patterns) {
|
|
242
|
+
for (const pattern of patterns) {
|
|
243
|
+
const matches = await (0, import_glob.glob)(pattern, { cwd, nodir: true });
|
|
244
|
+
if (matches.length > 0) {
|
|
245
|
+
return import_path.default.join(cwd, matches[0]);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/detection/detector.ts
|
|
252
|
+
async function detectFramework(cwd) {
|
|
253
|
+
const signals = [];
|
|
254
|
+
const pkg = await readPackageJson(cwd);
|
|
255
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
256
|
+
if (await fileExists(import_path2.default.join(cwd, "Package.swift"))) {
|
|
257
|
+
signals.push({ framework: "ios", score: 100, signal: "Package.swift found" });
|
|
258
|
+
}
|
|
259
|
+
const xcodeprojs = await findFile(cwd, ["*.xcodeproj", "*.xcworkspace"]);
|
|
260
|
+
if (xcodeprojs) {
|
|
261
|
+
signals.push({ framework: "ios", score: 90, signal: "Xcode project found" });
|
|
262
|
+
}
|
|
263
|
+
if (!pkg) {
|
|
264
|
+
const iosScore = signals.filter((s) => s.framework === "ios").reduce((a, b) => a + b.score, 0);
|
|
265
|
+
if (iosScore > 0) {
|
|
266
|
+
return buildResult("ios", signals, null);
|
|
267
|
+
}
|
|
268
|
+
return buildResult("unknown", [], null);
|
|
269
|
+
}
|
|
270
|
+
if (deps.next) {
|
|
271
|
+
signals.push({ framework: "nextjs", score: 100, signal: "next in dependencies" });
|
|
272
|
+
if (await fileExists(import_path2.default.join(cwd, "app", "layout.tsx")) || await fileExists(import_path2.default.join(cwd, "app", "layout.js"))) {
|
|
273
|
+
signals.push({ framework: "nextjs", score: 20, signal: "App Router detected" });
|
|
274
|
+
}
|
|
275
|
+
if (await fileExists(import_path2.default.join(cwd, "pages", "_app.tsx")) || await fileExists(import_path2.default.join(cwd, "pages", "_app.js"))) {
|
|
276
|
+
signals.push({ framework: "nextjs", score: 15, signal: "Pages Router detected" });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (deps.expo) {
|
|
280
|
+
signals.push({ framework: "expo", score: 100, signal: "expo in dependencies" });
|
|
281
|
+
}
|
|
282
|
+
if (deps["react-native"] && !deps.expo) {
|
|
283
|
+
signals.push({ framework: "react-native", score: 100, signal: "react-native in dependencies" });
|
|
284
|
+
}
|
|
285
|
+
if (deps["@remix-run/react"] || deps["@remix-run/node"]) {
|
|
286
|
+
signals.push({ framework: "remix", score: 100, signal: "@remix-run packages found" });
|
|
287
|
+
}
|
|
288
|
+
if (deps.astro) {
|
|
289
|
+
signals.push({ framework: "astro", score: 100, signal: "astro in dependencies" });
|
|
290
|
+
}
|
|
291
|
+
if (deps["@sveltejs/kit"]) {
|
|
292
|
+
signals.push({ framework: "sveltekit", score: 100, signal: "@sveltejs/kit in dependencies" });
|
|
293
|
+
} else if (deps.svelte) {
|
|
294
|
+
signals.push({ framework: "svelte", score: 80, signal: "svelte in dependencies" });
|
|
295
|
+
}
|
|
296
|
+
if (deps.nuxt) {
|
|
297
|
+
signals.push({ framework: "nuxt", score: 100, signal: "nuxt in dependencies" });
|
|
298
|
+
} else if (deps.vue) {
|
|
299
|
+
signals.push({ framework: "vue", score: 80, signal: "vue in dependencies" });
|
|
300
|
+
}
|
|
301
|
+
if (deps.react && deps["react-dom"]) {
|
|
302
|
+
if (!deps.next && !deps["@remix-run/react"] && !deps["react-native"] && !deps.expo) {
|
|
303
|
+
if (deps.vite || deps["@vitejs/plugin-react"]) {
|
|
304
|
+
signals.push({ framework: "react-vite", score: 90, signal: "React + Vite" });
|
|
305
|
+
} else if (deps["react-scripts"]) {
|
|
306
|
+
signals.push({ framework: "react", score: 85, signal: "Create React App" });
|
|
307
|
+
} else {
|
|
308
|
+
signals.push({ framework: "react", score: 70, signal: "React project" });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (deps.express || deps.fastify || deps.koa || deps.hapi) {
|
|
313
|
+
if (!deps.react && !deps.vue && !deps.svelte) {
|
|
314
|
+
signals.push({ framework: "node", score: 80, signal: "Node.js server framework" });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const frameworkScores = /* @__PURE__ */ new Map();
|
|
318
|
+
signals.forEach((s) => {
|
|
319
|
+
const current = frameworkScores.get(s.framework) || 0;
|
|
320
|
+
frameworkScores.set(s.framework, current + s.score);
|
|
321
|
+
});
|
|
322
|
+
let bestFramework = "unknown";
|
|
323
|
+
let bestScore = 0;
|
|
324
|
+
frameworkScores.forEach((score, framework) => {
|
|
325
|
+
if (score > bestScore) {
|
|
326
|
+
bestScore = score;
|
|
327
|
+
bestFramework = framework;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
return buildResult(bestFramework, signals, pkg);
|
|
331
|
+
}
|
|
332
|
+
function buildResult(framework, signals, pkg) {
|
|
333
|
+
const relevantSignals = signals.filter((s) => s.framework === framework).map((s) => s.signal);
|
|
334
|
+
const confidence = Math.min(100, signals.filter((s) => s.framework === framework).reduce((a, b) => a + b.score, 0));
|
|
335
|
+
const language = detectLanguage(pkg);
|
|
336
|
+
const sdks = getSDKsForFramework(framework);
|
|
337
|
+
const hasAppRouter = framework === "nextjs" && relevantSignals.some((s) => s.includes("App Router"));
|
|
338
|
+
return {
|
|
339
|
+
framework,
|
|
340
|
+
confidence,
|
|
341
|
+
signals: relevantSignals,
|
|
342
|
+
sdks,
|
|
343
|
+
language,
|
|
344
|
+
hasAppRouter,
|
|
345
|
+
isExpo: framework === "expo"
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function detectLanguage(pkg) {
|
|
349
|
+
if (!pkg) return "javascript";
|
|
350
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
351
|
+
return deps.typescript ? "typescript" : "javascript";
|
|
352
|
+
}
|
|
353
|
+
function getSDKsForFramework(framework) {
|
|
354
|
+
switch (framework) {
|
|
355
|
+
case "nextjs":
|
|
356
|
+
case "remix":
|
|
357
|
+
case "sveltekit":
|
|
358
|
+
case "nuxt":
|
|
359
|
+
case "astro":
|
|
360
|
+
return ["@datalyr/web", "@datalyr/api"];
|
|
361
|
+
case "react":
|
|
362
|
+
case "react-vite":
|
|
363
|
+
case "svelte":
|
|
364
|
+
case "vue":
|
|
365
|
+
return ["@datalyr/web"];
|
|
366
|
+
case "react-native":
|
|
367
|
+
case "expo":
|
|
368
|
+
return ["@datalyr/react-native"];
|
|
369
|
+
case "ios":
|
|
370
|
+
return ["DatalyrSDK"];
|
|
371
|
+
case "node":
|
|
372
|
+
return ["@datalyr/api"];
|
|
373
|
+
default:
|
|
374
|
+
return ["@datalyr/web"];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function getFrameworkDisplayName(framework) {
|
|
378
|
+
const names = {
|
|
379
|
+
nextjs: "Next.js",
|
|
380
|
+
react: "React (Create React App)",
|
|
381
|
+
"react-vite": "React (Vite)",
|
|
382
|
+
svelte: "Svelte",
|
|
383
|
+
sveltekit: "SvelteKit",
|
|
384
|
+
vue: "Vue",
|
|
385
|
+
nuxt: "Nuxt",
|
|
386
|
+
remix: "Remix",
|
|
387
|
+
astro: "Astro",
|
|
388
|
+
"react-native": "React Native",
|
|
389
|
+
expo: "Expo",
|
|
390
|
+
ios: "iOS (Swift)",
|
|
391
|
+
node: "Node.js",
|
|
392
|
+
unknown: "Unknown"
|
|
393
|
+
};
|
|
394
|
+
return names[framework];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/agent/docs/index.ts
|
|
398
|
+
function getFrameworkDocs(framework, apiKey) {
|
|
399
|
+
switch (framework) {
|
|
400
|
+
case "nextjs":
|
|
401
|
+
return getNextjsDocs(apiKey);
|
|
402
|
+
case "react":
|
|
403
|
+
case "react-vite":
|
|
404
|
+
return getReactDocs(apiKey);
|
|
405
|
+
case "sveltekit":
|
|
406
|
+
return getSvelteKitDocs(apiKey);
|
|
407
|
+
case "svelte":
|
|
408
|
+
return getSvelteDocs(apiKey);
|
|
409
|
+
case "react-native":
|
|
410
|
+
case "expo":
|
|
411
|
+
return getReactNativeDocs(apiKey, framework === "expo");
|
|
412
|
+
case "ios":
|
|
413
|
+
return getIOSDocs(apiKey);
|
|
414
|
+
case "node":
|
|
415
|
+
return getNodeDocs(apiKey);
|
|
416
|
+
default:
|
|
417
|
+
return getGenericWebDocs(apiKey);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function getNextjsDocs(apiKey) {
|
|
421
|
+
return `
|
|
422
|
+
# Datalyr Next.js Integration
|
|
423
|
+
|
|
424
|
+
## Packages to Install
|
|
425
|
+
- @datalyr/web (client-side tracking)
|
|
426
|
+
- @datalyr/api (server-side tracking)
|
|
427
|
+
|
|
428
|
+
## Environment Variables
|
|
429
|
+
Add to .env.local:
|
|
430
|
+
\`\`\`
|
|
431
|
+
NEXT_PUBLIC_DATALYR_WORKSPACE_ID=your_workspace_id
|
|
432
|
+
DATALYR_API_KEY=${apiKey}
|
|
433
|
+
\`\`\`
|
|
434
|
+
|
|
435
|
+
## App Router Setup (Next.js 13+)
|
|
436
|
+
|
|
437
|
+
### 1. Create Provider Component
|
|
438
|
+
Create \`app/providers.tsx\`:
|
|
439
|
+
\`\`\`tsx
|
|
440
|
+
'use client';
|
|
441
|
+
|
|
442
|
+
import datalyr from '@datalyr/web';
|
|
443
|
+
import { useEffect } from 'react';
|
|
444
|
+
|
|
445
|
+
export function DatalyrProvider({ children }: { children: React.ReactNode }) {
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
datalyr.init({
|
|
448
|
+
workspaceId: process.env.NEXT_PUBLIC_DATALYR_WORKSPACE_ID!,
|
|
449
|
+
debug: process.env.NODE_ENV === 'development',
|
|
450
|
+
trackSPA: true,
|
|
451
|
+
});
|
|
452
|
+
}, []);
|
|
453
|
+
|
|
454
|
+
return <>{children}</>;
|
|
455
|
+
}
|
|
456
|
+
\`\`\`
|
|
457
|
+
|
|
458
|
+
### 2. Wrap Layout
|
|
459
|
+
Update \`app/layout.tsx\` to wrap children with the provider:
|
|
460
|
+
\`\`\`tsx
|
|
461
|
+
import { DatalyrProvider } from './providers';
|
|
462
|
+
|
|
463
|
+
export default function RootLayout({ children }) {
|
|
464
|
+
return (
|
|
465
|
+
<html>
|
|
466
|
+
<body>
|
|
467
|
+
<DatalyrProvider>
|
|
468
|
+
{children}
|
|
469
|
+
</DatalyrProvider>
|
|
470
|
+
</body>
|
|
471
|
+
</html>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
\`\`\`
|
|
475
|
+
|
|
476
|
+
### 3. Server-Side Instance (Optional)
|
|
477
|
+
Create \`lib/datalyr.ts\` for server-side tracking:
|
|
478
|
+
\`\`\`ts
|
|
479
|
+
import Datalyr from '@datalyr/api';
|
|
480
|
+
|
|
481
|
+
export const datalyr = new Datalyr(process.env.DATALYR_API_KEY!);
|
|
482
|
+
\`\`\`
|
|
483
|
+
|
|
484
|
+
## Pages Router Setup (Legacy)
|
|
485
|
+
|
|
486
|
+
### 1. Create Hook
|
|
487
|
+
Create \`lib/datalyr.ts\`:
|
|
488
|
+
\`\`\`ts
|
|
489
|
+
import datalyr from '@datalyr/web';
|
|
490
|
+
|
|
491
|
+
let initialized = false;
|
|
492
|
+
|
|
493
|
+
export function initDatalyr() {
|
|
494
|
+
if (initialized || typeof window === 'undefined') return;
|
|
495
|
+
|
|
496
|
+
datalyr.init({
|
|
497
|
+
workspaceId: process.env.NEXT_PUBLIC_DATALYR_WORKSPACE_ID!,
|
|
498
|
+
debug: process.env.NODE_ENV === 'development',
|
|
499
|
+
trackSPA: true,
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
initialized = true;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export { datalyr };
|
|
506
|
+
\`\`\`
|
|
507
|
+
|
|
508
|
+
### 2. Initialize in _app.tsx
|
|
509
|
+
\`\`\`tsx
|
|
510
|
+
import { useEffect } from 'react';
|
|
511
|
+
import { initDatalyr } from '../lib/datalyr';
|
|
512
|
+
|
|
513
|
+
function MyApp({ Component, pageProps }) {
|
|
514
|
+
useEffect(() => {
|
|
515
|
+
initDatalyr();
|
|
516
|
+
}, []);
|
|
517
|
+
|
|
518
|
+
return <Component {...pageProps} />;
|
|
519
|
+
}
|
|
520
|
+
\`\`\`
|
|
521
|
+
`;
|
|
522
|
+
}
|
|
523
|
+
function getReactDocs(apiKey) {
|
|
524
|
+
return `
|
|
525
|
+
# Datalyr React Integration
|
|
526
|
+
|
|
527
|
+
## Package to Install
|
|
528
|
+
- @datalyr/web
|
|
529
|
+
|
|
530
|
+
## Environment Variables
|
|
531
|
+
Add to .env (or .env.local for Vite):
|
|
532
|
+
\`\`\`
|
|
533
|
+
VITE_DATALYR_WORKSPACE_ID=your_workspace_id
|
|
534
|
+
# Or for Create React App:
|
|
535
|
+
REACT_APP_DATALYR_WORKSPACE_ID=your_workspace_id
|
|
536
|
+
\`\`\`
|
|
537
|
+
|
|
538
|
+
## Setup
|
|
539
|
+
|
|
540
|
+
### 1. Create Initialization Module
|
|
541
|
+
Create \`src/lib/datalyr.ts\`:
|
|
542
|
+
\`\`\`ts
|
|
543
|
+
import datalyr from '@datalyr/web';
|
|
544
|
+
|
|
545
|
+
let initialized = false;
|
|
546
|
+
|
|
547
|
+
export function initDatalyr() {
|
|
548
|
+
if (initialized || typeof window === 'undefined') return;
|
|
549
|
+
|
|
550
|
+
datalyr.init({
|
|
551
|
+
// Vite uses import.meta.env, CRA uses process.env
|
|
552
|
+
workspaceId: import.meta.env?.VITE_DATALYR_WORKSPACE_ID ||
|
|
553
|
+
process.env.REACT_APP_DATALYR_WORKSPACE_ID,
|
|
554
|
+
debug: import.meta.env?.DEV || process.env.NODE_ENV === 'development',
|
|
555
|
+
trackSPA: true,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
initialized = true;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export { datalyr };
|
|
562
|
+
\`\`\`
|
|
563
|
+
|
|
564
|
+
### 2. Initialize in Entry Point
|
|
565
|
+
Update \`src/main.tsx\` (Vite) or \`src/index.tsx\` (CRA):
|
|
566
|
+
\`\`\`tsx
|
|
567
|
+
import { initDatalyr } from './lib/datalyr';
|
|
568
|
+
|
|
569
|
+
// Initialize before rendering
|
|
570
|
+
initDatalyr();
|
|
571
|
+
|
|
572
|
+
// ... rest of your app setup
|
|
573
|
+
\`\`\`
|
|
574
|
+
|
|
575
|
+
## Tracking Events
|
|
576
|
+
\`\`\`ts
|
|
577
|
+
import { datalyr } from './lib/datalyr';
|
|
578
|
+
|
|
579
|
+
// Track an event
|
|
580
|
+
datalyr.track('button_clicked', { button_id: 'signup' });
|
|
581
|
+
|
|
582
|
+
// Identify a user
|
|
583
|
+
datalyr.identify('user_123', { email: 'user@example.com' });
|
|
584
|
+
\`\`\`
|
|
585
|
+
`;
|
|
586
|
+
}
|
|
587
|
+
function getSvelteKitDocs(apiKey) {
|
|
588
|
+
return `
|
|
589
|
+
# Datalyr SvelteKit Integration
|
|
590
|
+
|
|
591
|
+
## Packages to Install
|
|
592
|
+
- @datalyr/web (client-side)
|
|
593
|
+
- @datalyr/api (server-side)
|
|
594
|
+
|
|
595
|
+
## Environment Variables
|
|
596
|
+
Add to .env:
|
|
597
|
+
\`\`\`
|
|
598
|
+
PUBLIC_DATALYR_WORKSPACE_ID=your_workspace_id
|
|
599
|
+
DATALYR_API_KEY=${apiKey}
|
|
600
|
+
\`\`\`
|
|
601
|
+
|
|
602
|
+
## Setup
|
|
603
|
+
|
|
604
|
+
### 1. Client-Side Initialization
|
|
605
|
+
Create \`src/lib/datalyr.ts\`:
|
|
606
|
+
\`\`\`ts
|
|
607
|
+
import datalyr from '@datalyr/web';
|
|
608
|
+
import { browser } from '$app/environment';
|
|
609
|
+
|
|
610
|
+
let initialized = false;
|
|
611
|
+
|
|
612
|
+
export function initDatalyr() {
|
|
613
|
+
if (initialized || !browser) return;
|
|
614
|
+
|
|
615
|
+
datalyr.init({
|
|
616
|
+
workspaceId: import.meta.env.PUBLIC_DATALYR_WORKSPACE_ID,
|
|
617
|
+
debug: import.meta.env.DEV,
|
|
618
|
+
trackSPA: true,
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
initialized = true;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export { datalyr };
|
|
625
|
+
\`\`\`
|
|
626
|
+
|
|
627
|
+
### 2. Initialize in Layout
|
|
628
|
+
Update \`src/routes/+layout.svelte\`:
|
|
629
|
+
\`\`\`svelte
|
|
630
|
+
<script>
|
|
631
|
+
import { onMount } from 'svelte';
|
|
632
|
+
import { initDatalyr } from '$lib/datalyr';
|
|
633
|
+
|
|
634
|
+
onMount(() => {
|
|
635
|
+
initDatalyr();
|
|
636
|
+
});
|
|
637
|
+
</script>
|
|
638
|
+
|
|
639
|
+
<slot />
|
|
640
|
+
\`\`\`
|
|
641
|
+
|
|
642
|
+
### 3. Server-Side Instance
|
|
643
|
+
Create \`src/lib/server/datalyr.ts\`:
|
|
644
|
+
\`\`\`ts
|
|
645
|
+
import Datalyr from '@datalyr/api';
|
|
646
|
+
import { DATALYR_API_KEY } from '$env/static/private';
|
|
647
|
+
|
|
648
|
+
export const datalyr = new Datalyr(DATALYR_API_KEY);
|
|
649
|
+
\`\`\`
|
|
650
|
+
`;
|
|
651
|
+
}
|
|
652
|
+
function getSvelteDocs(apiKey) {
|
|
653
|
+
return `
|
|
654
|
+
# Datalyr Svelte Integration
|
|
655
|
+
|
|
656
|
+
## Package to Install
|
|
657
|
+
- @datalyr/web
|
|
658
|
+
|
|
659
|
+
## Environment Variables
|
|
660
|
+
Add to .env:
|
|
661
|
+
\`\`\`
|
|
662
|
+
VITE_DATALYR_WORKSPACE_ID=your_workspace_id
|
|
663
|
+
\`\`\`
|
|
664
|
+
|
|
665
|
+
## Setup
|
|
666
|
+
|
|
667
|
+
### 1. Create Initialization Module
|
|
668
|
+
Create \`src/lib/datalyr.ts\`:
|
|
669
|
+
\`\`\`ts
|
|
670
|
+
import datalyr from '@datalyr/web';
|
|
671
|
+
|
|
672
|
+
let initialized = false;
|
|
673
|
+
|
|
674
|
+
export function initDatalyr() {
|
|
675
|
+
if (initialized || typeof window === 'undefined') return;
|
|
676
|
+
|
|
677
|
+
datalyr.init({
|
|
678
|
+
workspaceId: import.meta.env.VITE_DATALYR_WORKSPACE_ID,
|
|
679
|
+
debug: import.meta.env.DEV,
|
|
680
|
+
trackSPA: true,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
initialized = true;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export { datalyr };
|
|
687
|
+
\`\`\`
|
|
688
|
+
|
|
689
|
+
### 2. Initialize in App
|
|
690
|
+
Update \`src/App.svelte\`:
|
|
691
|
+
\`\`\`svelte
|
|
692
|
+
<script>
|
|
693
|
+
import { onMount } from 'svelte';
|
|
694
|
+
import { initDatalyr } from './lib/datalyr';
|
|
695
|
+
|
|
696
|
+
onMount(() => {
|
|
697
|
+
initDatalyr();
|
|
698
|
+
});
|
|
699
|
+
</script>
|
|
700
|
+
|
|
701
|
+
<!-- Your app content -->
|
|
702
|
+
\`\`\`
|
|
703
|
+
`;
|
|
704
|
+
}
|
|
705
|
+
function getReactNativeDocs(_apiKey, isExpo) {
|
|
706
|
+
const importPath = isExpo ? "@datalyr/react-native/expo" : "@datalyr/react-native";
|
|
707
|
+
const configImport = isExpo ? "import Constants from 'expo-constants';" : "";
|
|
708
|
+
const configAccess = isExpo ? "Constants.expoConfig?.extra?.datalyrApiKey || ''" : "process.env.DATALYR_API_KEY || ''";
|
|
709
|
+
return `
|
|
710
|
+
# Datalyr React Native Integration${isExpo ? " (Expo)" : ""}
|
|
711
|
+
|
|
712
|
+
## Package to Install
|
|
713
|
+
- @datalyr/react-native
|
|
714
|
+
${isExpo ? "- expo-constants (for config access)" : ""}
|
|
715
|
+
|
|
716
|
+
## Post-Install (iOS)
|
|
717
|
+
After installing, run:
|
|
718
|
+
\`\`\`bash
|
|
719
|
+
cd ios && pod install
|
|
720
|
+
\`\`\`
|
|
721
|
+
|
|
722
|
+
## Environment Configuration
|
|
723
|
+
${isExpo ? `
|
|
724
|
+
### For Expo
|
|
725
|
+
Add to \`app.config.js\` or \`app.json\`:
|
|
726
|
+
\`\`\`js
|
|
727
|
+
export default {
|
|
728
|
+
expo: {
|
|
729
|
+
extra: {
|
|
730
|
+
datalyrApiKey: process.env.DATALYR_API_KEY,
|
|
731
|
+
datalyrWorkspaceId: process.env.DATALYR_WORKSPACE_ID,
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
\`\`\`
|
|
736
|
+
|
|
737
|
+
Create \`.env\` file:
|
|
738
|
+
\`\`\`
|
|
739
|
+
DATALYR_API_KEY=your_api_key
|
|
740
|
+
DATALYR_WORKSPACE_ID=your_workspace_id
|
|
741
|
+
\`\`\`
|
|
742
|
+
` : `
|
|
743
|
+
### For React Native CLI
|
|
744
|
+
Create \`.env\` file and use react-native-config:
|
|
745
|
+
\`\`\`
|
|
746
|
+
DATALYR_API_KEY=your_api_key
|
|
747
|
+
DATALYR_WORKSPACE_ID=your_workspace_id
|
|
748
|
+
\`\`\`
|
|
749
|
+
`}
|
|
750
|
+
**Security Note**: Never commit API keys to source control. Use environment variables or secrets management.
|
|
751
|
+
|
|
752
|
+
## Setup
|
|
753
|
+
|
|
754
|
+
### 1. Create Initialization Module
|
|
755
|
+
Create \`src/utils/datalyr.ts\`:
|
|
756
|
+
\`\`\`ts
|
|
757
|
+
import { Datalyr } from '${importPath}';
|
|
758
|
+
${configImport}
|
|
759
|
+
|
|
760
|
+
let initialized = false;
|
|
761
|
+
|
|
762
|
+
export async function initDatalyr() {
|
|
763
|
+
if (initialized) return;
|
|
764
|
+
|
|
765
|
+
const apiKey = ${configAccess};
|
|
766
|
+
|
|
767
|
+
if (!apiKey) {
|
|
768
|
+
console.warn('[Datalyr] API key not configured');
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
await Datalyr.initialize({
|
|
773
|
+
apiKey,
|
|
774
|
+
enableAutoEvents: true,
|
|
775
|
+
enableAttribution: true,
|
|
776
|
+
debug: __DEV__,
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
initialized = true;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
export { Datalyr };
|
|
783
|
+
\`\`\`
|
|
784
|
+
|
|
785
|
+
### 2. Initialize in App
|
|
786
|
+
Update \`App.tsx\`:
|
|
787
|
+
\`\`\`tsx
|
|
788
|
+
import { useEffect } from 'react';
|
|
789
|
+
import { initDatalyr } from './src/utils/datalyr';
|
|
790
|
+
|
|
791
|
+
export default function App() {
|
|
792
|
+
useEffect(() => {
|
|
793
|
+
initDatalyr();
|
|
794
|
+
}, []);
|
|
795
|
+
|
|
796
|
+
return (
|
|
797
|
+
// Your app content
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
\`\`\`
|
|
801
|
+
|
|
802
|
+
## Tracking Events
|
|
803
|
+
\`\`\`ts
|
|
804
|
+
import { Datalyr } from './src/utils/datalyr';
|
|
805
|
+
|
|
806
|
+
// Track an event
|
|
807
|
+
Datalyr.track('purchase_completed', { amount: 99.99 });
|
|
808
|
+
|
|
809
|
+
// Identify a user
|
|
810
|
+
Datalyr.identify('user_123', { email: 'user@example.com' });
|
|
811
|
+
\`\`\`
|
|
812
|
+
`;
|
|
813
|
+
}
|
|
814
|
+
function getNodeDocs(apiKey) {
|
|
815
|
+
return `
|
|
816
|
+
# Datalyr Node.js Integration
|
|
817
|
+
|
|
818
|
+
## Package to Install
|
|
819
|
+
- @datalyr/api
|
|
820
|
+
|
|
821
|
+
## Environment Variables
|
|
822
|
+
Add to .env:
|
|
823
|
+
\`\`\`
|
|
824
|
+
DATALYR_API_KEY=${apiKey}
|
|
825
|
+
\`\`\`
|
|
826
|
+
|
|
827
|
+
## Setup
|
|
828
|
+
|
|
829
|
+
### 1. Create Datalyr Instance
|
|
830
|
+
Create \`src/lib/datalyr.ts\`:
|
|
831
|
+
\`\`\`ts
|
|
832
|
+
import Datalyr from '@datalyr/api';
|
|
833
|
+
|
|
834
|
+
const apiKey = process.env.DATALYR_API_KEY;
|
|
835
|
+
if (!apiKey) {
|
|
836
|
+
throw new Error('DATALYR_API_KEY is required');
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
export const datalyr = new Datalyr(apiKey);
|
|
840
|
+
\`\`\`
|
|
841
|
+
|
|
842
|
+
### 2. Track Events
|
|
843
|
+
\`\`\`ts
|
|
844
|
+
import { datalyr } from './lib/datalyr';
|
|
845
|
+
|
|
846
|
+
// Track server-side events
|
|
847
|
+
await datalyr.track({
|
|
848
|
+
event: 'order_completed',
|
|
849
|
+
userId: 'user_123',
|
|
850
|
+
properties: {
|
|
851
|
+
orderId: 'order_456',
|
|
852
|
+
amount: 99.99,
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
// Identify a user
|
|
857
|
+
await datalyr.identify({
|
|
858
|
+
userId: 'user_123',
|
|
859
|
+
traits: {
|
|
860
|
+
email: 'user@example.com',
|
|
861
|
+
plan: 'premium',
|
|
862
|
+
},
|
|
863
|
+
});
|
|
864
|
+
\`\`\`
|
|
865
|
+
`;
|
|
866
|
+
}
|
|
867
|
+
function getIOSDocs(_apiKey) {
|
|
868
|
+
return `
|
|
869
|
+
# Datalyr iOS (Swift) Integration
|
|
870
|
+
|
|
871
|
+
## Package to Install
|
|
872
|
+
Add via Swift Package Manager:
|
|
873
|
+
- https://github.com/datalyr/datalyr-ios-sdk
|
|
874
|
+
|
|
875
|
+
## Setup
|
|
876
|
+
|
|
877
|
+
### 1. Add Keys to Info.plist
|
|
878
|
+
Add both keys to your Info.plist (or use xcconfig for different environments):
|
|
879
|
+
\`\`\`xml
|
|
880
|
+
<key>DATALYR_WORKSPACE_ID</key>
|
|
881
|
+
<string>YOUR_WORKSPACE_ID</string>
|
|
882
|
+
<key>DATALYR_API_KEY</key>
|
|
883
|
+
<string>YOUR_API_KEY</string>
|
|
884
|
+
\`\`\`
|
|
885
|
+
|
|
886
|
+
**Security Note**: For production apps, consider using xcconfig files or build-time environment variables to inject these values, keeping them out of source control.
|
|
887
|
+
|
|
888
|
+
### 2. Add Configuration File
|
|
889
|
+
Create \`DatalyrConfig.swift\`:
|
|
890
|
+
\`\`\`swift
|
|
891
|
+
import DatalyrSDK
|
|
892
|
+
import Foundation
|
|
893
|
+
|
|
894
|
+
struct DatalyrConfig {
|
|
895
|
+
static func initialize() {
|
|
896
|
+
guard let workspaceId = Bundle.main.object(forInfoDictionaryKey: "DATALYR_WORKSPACE_ID") as? String,
|
|
897
|
+
let apiKey = Bundle.main.object(forInfoDictionaryKey: "DATALYR_API_KEY") as? String else {
|
|
898
|
+
#if DEBUG
|
|
899
|
+
fatalError("DATALYR_WORKSPACE_ID or DATALYR_API_KEY not found in Info.plist")
|
|
900
|
+
#else
|
|
901
|
+
print("[Datalyr] Warning: SDK not configured - missing Info.plist keys")
|
|
902
|
+
return
|
|
903
|
+
#endif
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
Datalyr.shared.configure(
|
|
907
|
+
apiKey: apiKey,
|
|
908
|
+
workspaceId: workspaceId,
|
|
909
|
+
options: DatalyrOptions(
|
|
910
|
+
debug: false,
|
|
911
|
+
enableAutoEvents: true,
|
|
912
|
+
enableAttribution: true
|
|
913
|
+
)
|
|
914
|
+
)
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
\`\`\`
|
|
918
|
+
|
|
919
|
+
### 3. Initialize in App
|
|
920
|
+
For SwiftUI (\`@main App\`):
|
|
921
|
+
\`\`\`swift
|
|
922
|
+
@main
|
|
923
|
+
struct MyApp: App {
|
|
924
|
+
init() {
|
|
925
|
+
DatalyrConfig.initialize()
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
var body: some Scene {
|
|
929
|
+
WindowGroup {
|
|
930
|
+
ContentView()
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
\`\`\`
|
|
935
|
+
|
|
936
|
+
For UIKit (AppDelegate):
|
|
937
|
+
\`\`\`swift
|
|
938
|
+
func application(_ application: UIApplication, didFinishLaunchingWithOptions...) -> Bool {
|
|
939
|
+
DatalyrConfig.initialize()
|
|
940
|
+
return true
|
|
941
|
+
}
|
|
942
|
+
\`\`\`
|
|
943
|
+
|
|
944
|
+
## Tracking Events
|
|
945
|
+
\`\`\`swift
|
|
946
|
+
import DatalyrSDK
|
|
947
|
+
|
|
948
|
+
// Track an event
|
|
949
|
+
Datalyr.shared.track("purchase_completed", properties: [
|
|
950
|
+
"amount": 99.99,
|
|
951
|
+
"currency": "USD"
|
|
952
|
+
])
|
|
953
|
+
|
|
954
|
+
// Identify a user
|
|
955
|
+
Datalyr.shared.identify("user_123", traits: [
|
|
956
|
+
"email": "user@example.com",
|
|
957
|
+
"plan": "premium"
|
|
958
|
+
])
|
|
959
|
+
\`\`\`
|
|
960
|
+
`;
|
|
961
|
+
}
|
|
962
|
+
function getGenericWebDocs(apiKey) {
|
|
963
|
+
return `
|
|
964
|
+
# Datalyr Web Integration
|
|
965
|
+
|
|
966
|
+
## Package to Install
|
|
967
|
+
- @datalyr/web
|
|
968
|
+
|
|
969
|
+
## Setup
|
|
970
|
+
|
|
971
|
+
### Via NPM
|
|
972
|
+
\`\`\`ts
|
|
973
|
+
import datalyr from '@datalyr/web';
|
|
974
|
+
|
|
975
|
+
datalyr.init({
|
|
976
|
+
workspaceId: 'YOUR_WORKSPACE_ID',
|
|
977
|
+
debug: true,
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
// Track events
|
|
981
|
+
datalyr.track('page_viewed', { page: '/home' });
|
|
982
|
+
\`\`\`
|
|
983
|
+
|
|
984
|
+
### Via Script Tag
|
|
985
|
+
\`\`\`html
|
|
986
|
+
<script src="https://track.datalyr.com/dl.js"
|
|
987
|
+
data-workspace-id="YOUR_WORKSPACE_ID">
|
|
988
|
+
</script>
|
|
989
|
+
\`\`\`
|
|
990
|
+
`;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// src/agent/configs/types.ts
|
|
994
|
+
var NEXTJS_CONFIG = {
|
|
995
|
+
id: "nextjs",
|
|
996
|
+
name: "Next.js",
|
|
997
|
+
detectPackage: "next",
|
|
998
|
+
sdks: ["@datalyr/web", "@datalyr/api"],
|
|
999
|
+
envVars: [
|
|
1000
|
+
{
|
|
1001
|
+
key: "NEXT_PUBLIC_DATALYR_WORKSPACE_ID",
|
|
1002
|
+
description: "Your Datalyr workspace ID",
|
|
1003
|
+
isPublic: true
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
key: "DATALYR_API_KEY",
|
|
1007
|
+
description: "Server-side API key",
|
|
1008
|
+
isPublic: false
|
|
1009
|
+
}
|
|
1010
|
+
],
|
|
1011
|
+
estimatedTime: 3,
|
|
1012
|
+
docsUrl: "https://docs.datalyr.com/sdks/nextjs",
|
|
1013
|
+
postInstallSteps: [
|
|
1014
|
+
"Add your workspace ID to .env.local",
|
|
1015
|
+
"Wrap your layout with DatalyrProvider",
|
|
1016
|
+
"Start tracking events with datalyr.track()"
|
|
1017
|
+
],
|
|
1018
|
+
routerDetection: {
|
|
1019
|
+
appRouter: ["app/layout.tsx", "app/layout.jsx", "app/layout.ts", "app/layout.js"],
|
|
1020
|
+
pagesRouter: ["pages/_app.tsx", "pages/_app.jsx", "pages/_app.ts", "pages/_app.js"]
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
var REACT_CONFIG = {
|
|
1024
|
+
id: "react",
|
|
1025
|
+
name: "React",
|
|
1026
|
+
detectPackage: "react",
|
|
1027
|
+
sdks: ["@datalyr/web"],
|
|
1028
|
+
envVars: [
|
|
1029
|
+
{
|
|
1030
|
+
key: "VITE_DATALYR_WORKSPACE_ID",
|
|
1031
|
+
description: "Your Datalyr workspace ID",
|
|
1032
|
+
isPublic: true
|
|
1033
|
+
}
|
|
1034
|
+
],
|
|
1035
|
+
estimatedTime: 2,
|
|
1036
|
+
docsUrl: "https://docs.datalyr.com/sdks/react",
|
|
1037
|
+
postInstallSteps: [
|
|
1038
|
+
"Add your workspace ID to .env",
|
|
1039
|
+
"Call initDatalyr() in your main file"
|
|
1040
|
+
]
|
|
1041
|
+
};
|
|
1042
|
+
var REACT_NATIVE_CONFIG = {
|
|
1043
|
+
id: "react-native",
|
|
1044
|
+
name: "React Native",
|
|
1045
|
+
detectPackage: "react-native",
|
|
1046
|
+
sdks: ["@datalyr/react-native"],
|
|
1047
|
+
envVars: [],
|
|
1048
|
+
estimatedTime: 5,
|
|
1049
|
+
docsUrl: "https://docs.datalyr.com/sdks/react-native",
|
|
1050
|
+
postInstallSteps: [
|
|
1051
|
+
"Run: cd ios && pod install",
|
|
1052
|
+
"Call initDatalyr() in App.tsx",
|
|
1053
|
+
"For attribution, configure Meta/TikTok app IDs"
|
|
1054
|
+
]
|
|
1055
|
+
};
|
|
1056
|
+
var EXPO_CONFIG = {
|
|
1057
|
+
id: "expo",
|
|
1058
|
+
name: "Expo",
|
|
1059
|
+
detectPackage: "expo",
|
|
1060
|
+
sdks: ["@datalyr/react-native"],
|
|
1061
|
+
envVars: [],
|
|
1062
|
+
estimatedTime: 3,
|
|
1063
|
+
docsUrl: "https://docs.datalyr.com/sdks/expo",
|
|
1064
|
+
postInstallSteps: [
|
|
1065
|
+
"Call initDatalyr() in App.tsx",
|
|
1066
|
+
"For attribution, configure Meta/TikTok app IDs in app.json"
|
|
1067
|
+
]
|
|
1068
|
+
};
|
|
1069
|
+
var SVELTEKIT_CONFIG = {
|
|
1070
|
+
id: "sveltekit",
|
|
1071
|
+
name: "SvelteKit",
|
|
1072
|
+
detectPackage: "@sveltejs/kit",
|
|
1073
|
+
sdks: ["@datalyr/web", "@datalyr/api"],
|
|
1074
|
+
envVars: [
|
|
1075
|
+
{
|
|
1076
|
+
key: "PUBLIC_DATALYR_WORKSPACE_ID",
|
|
1077
|
+
description: "Your Datalyr workspace ID",
|
|
1078
|
+
isPublic: true
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
key: "DATALYR_API_KEY",
|
|
1082
|
+
description: "Server-side API key",
|
|
1083
|
+
isPublic: false
|
|
1084
|
+
}
|
|
1085
|
+
],
|
|
1086
|
+
estimatedTime: 3,
|
|
1087
|
+
docsUrl: "https://docs.datalyr.com/sdks/sveltekit",
|
|
1088
|
+
postInstallSteps: [
|
|
1089
|
+
"Add your workspace ID to .env",
|
|
1090
|
+
"Call initDatalyr() in +layout.svelte"
|
|
1091
|
+
]
|
|
1092
|
+
};
|
|
1093
|
+
var NODE_CONFIG = {
|
|
1094
|
+
id: "node",
|
|
1095
|
+
name: "Node.js",
|
|
1096
|
+
detectPackage: "express",
|
|
1097
|
+
sdks: ["@datalyr/api"],
|
|
1098
|
+
envVars: [
|
|
1099
|
+
{
|
|
1100
|
+
key: "DATALYR_API_KEY",
|
|
1101
|
+
description: "Your Datalyr API key",
|
|
1102
|
+
isPublic: false
|
|
1103
|
+
}
|
|
1104
|
+
],
|
|
1105
|
+
estimatedTime: 2,
|
|
1106
|
+
docsUrl: "https://docs.datalyr.com/sdks/node",
|
|
1107
|
+
postInstallSteps: [
|
|
1108
|
+
"Add your API key to .env",
|
|
1109
|
+
"Track events with datalyr.track()"
|
|
1110
|
+
]
|
|
1111
|
+
};
|
|
1112
|
+
var VUE_CONFIG = {
|
|
1113
|
+
id: "vue",
|
|
1114
|
+
name: "Vue.js",
|
|
1115
|
+
detectPackage: "vue",
|
|
1116
|
+
sdks: ["@datalyr/web"],
|
|
1117
|
+
envVars: [
|
|
1118
|
+
{
|
|
1119
|
+
key: "VITE_DATALYR_WORKSPACE_ID",
|
|
1120
|
+
description: "Your Datalyr workspace ID",
|
|
1121
|
+
isPublic: true
|
|
1122
|
+
}
|
|
1123
|
+
],
|
|
1124
|
+
estimatedTime: 2,
|
|
1125
|
+
docsUrl: "https://docs.datalyr.com/sdks/vue",
|
|
1126
|
+
postInstallSteps: [
|
|
1127
|
+
"Add your workspace ID to .env",
|
|
1128
|
+
"Call datalyr.init() in main.ts",
|
|
1129
|
+
"Track events with datalyr.track()"
|
|
1130
|
+
]
|
|
1131
|
+
};
|
|
1132
|
+
var NUXT_CONFIG = {
|
|
1133
|
+
id: "nuxt",
|
|
1134
|
+
name: "Nuxt",
|
|
1135
|
+
detectPackage: "nuxt",
|
|
1136
|
+
sdks: ["@datalyr/web", "@datalyr/api"],
|
|
1137
|
+
envVars: [
|
|
1138
|
+
{
|
|
1139
|
+
key: "NUXT_PUBLIC_DATALYR_WORKSPACE_ID",
|
|
1140
|
+
description: "Your Datalyr workspace ID",
|
|
1141
|
+
isPublic: true
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
key: "DATALYR_API_KEY",
|
|
1145
|
+
description: "Server-side API key",
|
|
1146
|
+
isPublic: false
|
|
1147
|
+
}
|
|
1148
|
+
],
|
|
1149
|
+
estimatedTime: 3,
|
|
1150
|
+
docsUrl: "https://docs.datalyr.com/sdks/nuxt",
|
|
1151
|
+
postInstallSteps: [
|
|
1152
|
+
"Add your workspace ID to .env",
|
|
1153
|
+
"Create a Nuxt plugin for initialization",
|
|
1154
|
+
"Track events with datalyr.track()"
|
|
1155
|
+
]
|
|
1156
|
+
};
|
|
1157
|
+
var REMIX_CONFIG = {
|
|
1158
|
+
id: "remix",
|
|
1159
|
+
name: "Remix",
|
|
1160
|
+
detectPackage: "@remix-run/react",
|
|
1161
|
+
sdks: ["@datalyr/web", "@datalyr/api"],
|
|
1162
|
+
envVars: [
|
|
1163
|
+
{
|
|
1164
|
+
key: "DATALYR_WORKSPACE_ID",
|
|
1165
|
+
description: "Your Datalyr workspace ID",
|
|
1166
|
+
isPublic: true
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
key: "DATALYR_API_KEY",
|
|
1170
|
+
description: "Server-side API key",
|
|
1171
|
+
isPublic: false
|
|
1172
|
+
}
|
|
1173
|
+
],
|
|
1174
|
+
estimatedTime: 3,
|
|
1175
|
+
docsUrl: "https://docs.datalyr.com/sdks/remix",
|
|
1176
|
+
postInstallSteps: [
|
|
1177
|
+
"Add your workspace ID to .env",
|
|
1178
|
+
"Initialize in root.tsx",
|
|
1179
|
+
"Track events with datalyr.track()"
|
|
1180
|
+
]
|
|
1181
|
+
};
|
|
1182
|
+
var ASTRO_CONFIG = {
|
|
1183
|
+
id: "astro",
|
|
1184
|
+
name: "Astro",
|
|
1185
|
+
detectPackage: "astro",
|
|
1186
|
+
sdks: ["@datalyr/web"],
|
|
1187
|
+
envVars: [
|
|
1188
|
+
{
|
|
1189
|
+
key: "PUBLIC_DATALYR_WORKSPACE_ID",
|
|
1190
|
+
description: "Your Datalyr workspace ID",
|
|
1191
|
+
isPublic: true
|
|
1192
|
+
}
|
|
1193
|
+
],
|
|
1194
|
+
estimatedTime: 2,
|
|
1195
|
+
docsUrl: "https://docs.datalyr.com/sdks/astro",
|
|
1196
|
+
postInstallSteps: [
|
|
1197
|
+
"Add your workspace ID to .env",
|
|
1198
|
+
"Add script to Layout component",
|
|
1199
|
+
"Track events with datalyr.track()"
|
|
1200
|
+
]
|
|
1201
|
+
};
|
|
1202
|
+
var IOS_CONFIG = {
|
|
1203
|
+
id: "ios",
|
|
1204
|
+
name: "iOS (Swift)",
|
|
1205
|
+
detectPackage: "",
|
|
1206
|
+
// Detected by Package.swift or .xcodeproj
|
|
1207
|
+
sdks: ["DatalyrSDK"],
|
|
1208
|
+
envVars: [
|
|
1209
|
+
{
|
|
1210
|
+
key: "DATALYR_WORKSPACE_ID",
|
|
1211
|
+
description: "Your Datalyr workspace ID (in Info.plist)",
|
|
1212
|
+
isPublic: false
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
key: "DATALYR_API_KEY",
|
|
1216
|
+
description: "Your Datalyr API key (in Info.plist)",
|
|
1217
|
+
isPublic: false
|
|
1218
|
+
}
|
|
1219
|
+
],
|
|
1220
|
+
estimatedTime: 5,
|
|
1221
|
+
docsUrl: "https://docs.datalyr.com/sdks/ios",
|
|
1222
|
+
postInstallSteps: [
|
|
1223
|
+
"Add DatalyrSDK via Swift Package Manager",
|
|
1224
|
+
"Add keys to Info.plist",
|
|
1225
|
+
"Call DatalyrConfig.initialize() in App init"
|
|
1226
|
+
]
|
|
1227
|
+
};
|
|
1228
|
+
var SVELTE_CONFIG = {
|
|
1229
|
+
id: "svelte",
|
|
1230
|
+
name: "Svelte",
|
|
1231
|
+
detectPackage: "svelte",
|
|
1232
|
+
sdks: ["@datalyr/web"],
|
|
1233
|
+
envVars: [
|
|
1234
|
+
{
|
|
1235
|
+
key: "VITE_DATALYR_WORKSPACE_ID",
|
|
1236
|
+
description: "Your Datalyr workspace ID",
|
|
1237
|
+
isPublic: true
|
|
1238
|
+
}
|
|
1239
|
+
],
|
|
1240
|
+
estimatedTime: 2,
|
|
1241
|
+
docsUrl: "https://docs.datalyr.com/sdks/svelte",
|
|
1242
|
+
postInstallSteps: [
|
|
1243
|
+
"Add your workspace ID to .env",
|
|
1244
|
+
"Call initDatalyr() in App.svelte"
|
|
1245
|
+
]
|
|
1246
|
+
};
|
|
1247
|
+
var FRAMEWORK_CONFIGS = {
|
|
1248
|
+
nextjs: NEXTJS_CONFIG,
|
|
1249
|
+
react: REACT_CONFIG,
|
|
1250
|
+
"react-vite": REACT_CONFIG,
|
|
1251
|
+
svelte: SVELTE_CONFIG,
|
|
1252
|
+
sveltekit: SVELTEKIT_CONFIG,
|
|
1253
|
+
vue: VUE_CONFIG,
|
|
1254
|
+
nuxt: NUXT_CONFIG,
|
|
1255
|
+
remix: REMIX_CONFIG,
|
|
1256
|
+
astro: ASTRO_CONFIG,
|
|
1257
|
+
"react-native": REACT_NATIVE_CONFIG,
|
|
1258
|
+
expo: EXPO_CONFIG,
|
|
1259
|
+
ios: IOS_CONFIG,
|
|
1260
|
+
node: NODE_CONFIG,
|
|
1261
|
+
unknown: void 0
|
|
1262
|
+
};
|
|
1263
|
+
function getSdksForFramework(framework) {
|
|
1264
|
+
const config = FRAMEWORK_CONFIGS[framework];
|
|
1265
|
+
if (!config) {
|
|
1266
|
+
return ["@datalyr/web"];
|
|
1267
|
+
}
|
|
1268
|
+
return config.sdks;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/agent/events/suggestions.ts
|
|
1272
|
+
var BUSINESS_TYPES = [
|
|
1273
|
+
{
|
|
1274
|
+
id: "saas",
|
|
1275
|
+
label: "SaaS / Web App",
|
|
1276
|
+
description: "Subscription or freemium product",
|
|
1277
|
+
hint: "Trials, signups, features, upgrades"
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
id: "mobile_app",
|
|
1281
|
+
label: "Mobile App",
|
|
1282
|
+
description: "Consumer or B2B mobile app",
|
|
1283
|
+
hint: "Installs, in-app events, attribution"
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
id: "lead_gen",
|
|
1287
|
+
label: "Lead Gen / Marketing",
|
|
1288
|
+
description: "Capture leads and conversions",
|
|
1289
|
+
hint: "Funnels, forms, demos, signups"
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
id: "b2b",
|
|
1293
|
+
label: "B2B Product",
|
|
1294
|
+
description: "Business software",
|
|
1295
|
+
hint: "Demos, trials, team invites, usage"
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
id: "agency",
|
|
1299
|
+
label: "Agency / Client Work",
|
|
1300
|
+
description: "Building for clients",
|
|
1301
|
+
hint: "Multi-site, white-label tracking"
|
|
1302
|
+
}
|
|
1303
|
+
];
|
|
1304
|
+
var EVENT_SUGGESTIONS = {
|
|
1305
|
+
saas: [
|
|
1306
|
+
{
|
|
1307
|
+
name: "signed_up",
|
|
1308
|
+
description: "User created an account",
|
|
1309
|
+
properties: ["method", "referrer"],
|
|
1310
|
+
priority: "high"
|
|
1311
|
+
},
|
|
1312
|
+
{
|
|
1313
|
+
name: "trial_started",
|
|
1314
|
+
description: "User started a free trial",
|
|
1315
|
+
properties: ["plan"],
|
|
1316
|
+
priority: "high"
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
name: "subscription_started",
|
|
1320
|
+
description: "User converted to paid",
|
|
1321
|
+
properties: ["plan", "value", "currency"],
|
|
1322
|
+
priority: "high"
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
name: "onboarding_completed",
|
|
1326
|
+
description: "User finished onboarding",
|
|
1327
|
+
properties: ["steps_completed"],
|
|
1328
|
+
priority: "medium"
|
|
1329
|
+
}
|
|
1330
|
+
],
|
|
1331
|
+
mobile_app: [
|
|
1332
|
+
{
|
|
1333
|
+
name: "signed_up",
|
|
1334
|
+
description: "User created an account",
|
|
1335
|
+
properties: ["method", "referrer"],
|
|
1336
|
+
priority: "high"
|
|
1337
|
+
},
|
|
1338
|
+
{
|
|
1339
|
+
name: "onboarding_started",
|
|
1340
|
+
description: "User started onboarding flow",
|
|
1341
|
+
properties: ["source"],
|
|
1342
|
+
priority: "high"
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
name: "onboarding_completed",
|
|
1346
|
+
description: "User finished onboarding",
|
|
1347
|
+
properties: ["steps_completed"],
|
|
1348
|
+
priority: "high"
|
|
1349
|
+
},
|
|
1350
|
+
{
|
|
1351
|
+
name: "paywall_viewed",
|
|
1352
|
+
description: "User saw the paywall/pricing",
|
|
1353
|
+
properties: ["source", "paywall_id"],
|
|
1354
|
+
priority: "high"
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
name: "trial_started",
|
|
1358
|
+
description: "User started a free trial",
|
|
1359
|
+
properties: ["plan"],
|
|
1360
|
+
priority: "high"
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
name: "subscription_started",
|
|
1364
|
+
description: "User converted to paid subscription",
|
|
1365
|
+
properties: ["plan", "value", "currency"],
|
|
1366
|
+
priority: "high"
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
name: "purchase",
|
|
1370
|
+
description: "User made in-app purchase",
|
|
1371
|
+
properties: ["product_id", "value", "currency"],
|
|
1372
|
+
priority: "medium"
|
|
1373
|
+
}
|
|
1374
|
+
],
|
|
1375
|
+
lead_gen: [
|
|
1376
|
+
{
|
|
1377
|
+
name: "lead",
|
|
1378
|
+
description: "User submitted their info",
|
|
1379
|
+
properties: ["form_name", "source"],
|
|
1380
|
+
priority: "high"
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
name: "signed_up",
|
|
1384
|
+
description: "User signed up / joined waitlist",
|
|
1385
|
+
properties: ["method", "referrer"],
|
|
1386
|
+
priority: "high"
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
name: "demo_requested",
|
|
1390
|
+
description: "User requested a demo",
|
|
1391
|
+
properties: ["product"],
|
|
1392
|
+
priority: "high"
|
|
1393
|
+
},
|
|
1394
|
+
{
|
|
1395
|
+
name: "cta_clicked",
|
|
1396
|
+
description: "User clicked a CTA",
|
|
1397
|
+
properties: ["button_name", "page"],
|
|
1398
|
+
priority: "medium"
|
|
1399
|
+
}
|
|
1400
|
+
],
|
|
1401
|
+
b2b: [
|
|
1402
|
+
{
|
|
1403
|
+
name: "signed_up",
|
|
1404
|
+
description: "User created an account",
|
|
1405
|
+
properties: ["method", "company_size"],
|
|
1406
|
+
priority: "high"
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
name: "demo_requested",
|
|
1410
|
+
description: "User requested a demo",
|
|
1411
|
+
properties: ["product", "company_size"],
|
|
1412
|
+
priority: "high"
|
|
1413
|
+
},
|
|
1414
|
+
{
|
|
1415
|
+
name: "trial_started",
|
|
1416
|
+
description: "User started a trial",
|
|
1417
|
+
properties: ["plan"],
|
|
1418
|
+
priority: "high"
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
name: "lead",
|
|
1422
|
+
description: "Lead captured",
|
|
1423
|
+
properties: ["source", "company_size"],
|
|
1424
|
+
priority: "high"
|
|
1425
|
+
}
|
|
1426
|
+
],
|
|
1427
|
+
agency: [
|
|
1428
|
+
{
|
|
1429
|
+
name: "lead",
|
|
1430
|
+
description: "Lead captured",
|
|
1431
|
+
properties: ["form_name", "client_id"],
|
|
1432
|
+
priority: "high"
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
name: "conversion",
|
|
1436
|
+
description: "Conversion event",
|
|
1437
|
+
properties: ["conversion_type", "value", "client_id"],
|
|
1438
|
+
priority: "high"
|
|
1439
|
+
},
|
|
1440
|
+
{
|
|
1441
|
+
name: "form_submitted",
|
|
1442
|
+
description: "User submitted a form",
|
|
1443
|
+
properties: ["form_name", "client_id"],
|
|
1444
|
+
priority: "high"
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
name: "cta_clicked",
|
|
1448
|
+
description: "User clicked a CTA",
|
|
1449
|
+
properties: ["button_name", "client_id"],
|
|
1450
|
+
priority: "medium"
|
|
1451
|
+
}
|
|
1452
|
+
]
|
|
1453
|
+
};
|
|
1454
|
+
function getEventSuggestions(businessType) {
|
|
1455
|
+
return EVENT_SUGGESTIONS[businessType] || EVENT_SUGGESTIONS.saas;
|
|
1456
|
+
}
|
|
1457
|
+
function formatEventDescription(event) {
|
|
1458
|
+
const propsDisplay = event.properties.length > 0 ? ` (${event.properties.join(", ")})` : "";
|
|
1459
|
+
return `${event.description}${propsDisplay}`;
|
|
1460
|
+
}
|
|
1461
|
+
function buildEventSelectOptions(events, includeAllOption = true) {
|
|
1462
|
+
const options = [];
|
|
1463
|
+
if (includeAllOption) {
|
|
1464
|
+
const highPriorityCount = events.filter((e) => e.priority === "high").length;
|
|
1465
|
+
options.push({
|
|
1466
|
+
value: "__all_recommended__",
|
|
1467
|
+
label: "All recommended events",
|
|
1468
|
+
hint: `Select all ${highPriorityCount} high-priority events`
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
for (const event of events) {
|
|
1472
|
+
const priorityBadge = event.priority === "high" ? "[recommended] " : "";
|
|
1473
|
+
options.push({
|
|
1474
|
+
value: event.name,
|
|
1475
|
+
label: event.name,
|
|
1476
|
+
hint: `${priorityBadge}${formatEventDescription(event)}`
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
return options;
|
|
1480
|
+
}
|
|
1481
|
+
function resolveSelectedEvents(selectedValues, allEvents) {
|
|
1482
|
+
if (selectedValues.includes("__all_recommended__")) {
|
|
1483
|
+
return allEvents.filter((e) => e.priority === "high");
|
|
1484
|
+
}
|
|
1485
|
+
return allEvents.filter((e) => selectedValues.includes(e.name));
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// src/agent/platform/config.ts
|
|
1489
|
+
var PLATFORM_TYPES = [
|
|
1490
|
+
{
|
|
1491
|
+
id: "web",
|
|
1492
|
+
label: "Web Only",
|
|
1493
|
+
description: "Website or web application",
|
|
1494
|
+
hint: "React, Next.js, Svelte, etc."
|
|
1495
|
+
},
|
|
1496
|
+
{
|
|
1497
|
+
id: "mobile",
|
|
1498
|
+
label: "Mobile Only",
|
|
1499
|
+
description: "iOS or Android app",
|
|
1500
|
+
hint: "React Native, Expo, Swift"
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
id: "both",
|
|
1504
|
+
label: "Web + Mobile",
|
|
1505
|
+
description: "Both web and mobile apps",
|
|
1506
|
+
hint: "Full cross-platform attribution"
|
|
1507
|
+
}
|
|
1508
|
+
];
|
|
1509
|
+
var AD_PLATFORMS = [
|
|
1510
|
+
{
|
|
1511
|
+
id: "meta",
|
|
1512
|
+
label: "Meta Ads (Facebook/Instagram)",
|
|
1513
|
+
description: "Track conversions from Meta campaigns",
|
|
1514
|
+
requiresServerSide: true,
|
|
1515
|
+
configKeys: ["META_PIXEL_ID", "META_ACCESS_TOKEN", "META_APP_ID"]
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
id: "google",
|
|
1519
|
+
label: "Google Ads",
|
|
1520
|
+
description: "Track conversions from Google campaigns",
|
|
1521
|
+
requiresServerSide: true,
|
|
1522
|
+
configKeys: ["GOOGLE_ADS_CUSTOMER_ID", "GOOGLE_ADS_CONVERSION_ID"]
|
|
1523
|
+
},
|
|
1524
|
+
{
|
|
1525
|
+
id: "tiktok",
|
|
1526
|
+
label: "TikTok Ads",
|
|
1527
|
+
description: "Track conversions from TikTok campaigns",
|
|
1528
|
+
requiresServerSide: true,
|
|
1529
|
+
configKeys: ["TIKTOK_PIXEL_ID", "TIKTOK_ACCESS_TOKEN", "TIKTOK_APP_ID"]
|
|
1530
|
+
},
|
|
1531
|
+
{
|
|
1532
|
+
id: "apple_search_ads",
|
|
1533
|
+
label: "Apple Search Ads",
|
|
1534
|
+
description: "Track iOS app install attribution",
|
|
1535
|
+
requiresServerSide: false,
|
|
1536
|
+
configKeys: []
|
|
1537
|
+
},
|
|
1538
|
+
{
|
|
1539
|
+
id: "none",
|
|
1540
|
+
label: "No ad platforms",
|
|
1541
|
+
description: "Skip ad platform setup",
|
|
1542
|
+
requiresServerSide: false,
|
|
1543
|
+
configKeys: []
|
|
1544
|
+
}
|
|
1545
|
+
];
|
|
1546
|
+
function getSdksForPlatform(platformType, adPlatforms) {
|
|
1547
|
+
const sdks = [];
|
|
1548
|
+
if (platformType === "web" || platformType === "both") {
|
|
1549
|
+
sdks.push("@datalyr/web");
|
|
1550
|
+
}
|
|
1551
|
+
if (platformType === "mobile" || platformType === "both") {
|
|
1552
|
+
sdks.push("@datalyr/react-native");
|
|
1553
|
+
}
|
|
1554
|
+
const needsServerSide = adPlatforms.some(
|
|
1555
|
+
(p2) => AD_PLATFORMS.find((ap) => ap.id === p2)?.requiresServerSide
|
|
1556
|
+
);
|
|
1557
|
+
if (needsServerSide) {
|
|
1558
|
+
sdks.push("@datalyr/api");
|
|
1559
|
+
}
|
|
1560
|
+
return sdks;
|
|
1561
|
+
}
|
|
1562
|
+
function getAttributionEvents(config) {
|
|
1563
|
+
const events = [];
|
|
1564
|
+
if (config.platformType === "mobile" || config.platformType === "both") {
|
|
1565
|
+
events.push({
|
|
1566
|
+
name: "app_install",
|
|
1567
|
+
description: "User installed the app (auto-tracked)",
|
|
1568
|
+
properties: ["attribution_source", "campaign_id", "ad_group_id"],
|
|
1569
|
+
serverSide: false
|
|
1570
|
+
});
|
|
1571
|
+
events.push({
|
|
1572
|
+
name: "app_open",
|
|
1573
|
+
description: "User opened the app",
|
|
1574
|
+
properties: ["source", "deep_link_url", "is_first_open"],
|
|
1575
|
+
serverSide: false
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
if (config.enableDeepLinking) {
|
|
1579
|
+
events.push({
|
|
1580
|
+
name: "deep_link_opened",
|
|
1581
|
+
description: "User opened a deep link",
|
|
1582
|
+
properties: ["url", "source", "campaign"],
|
|
1583
|
+
serverSide: false
|
|
1584
|
+
});
|
|
1585
|
+
events.push({
|
|
1586
|
+
name: "deferred_deep_link",
|
|
1587
|
+
description: "User installed via deferred deep link",
|
|
1588
|
+
properties: ["original_url", "install_time", "open_time"],
|
|
1589
|
+
serverSide: false
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
if (config.enableServerSideConversions) {
|
|
1593
|
+
if (config.adPlatforms.includes("meta")) {
|
|
1594
|
+
events.push({
|
|
1595
|
+
name: "purchase",
|
|
1596
|
+
description: "Purchase event for Meta CAPI",
|
|
1597
|
+
properties: ["value", "currency", "email", "phone", "fbc", "fbp"],
|
|
1598
|
+
serverSide: true
|
|
1599
|
+
});
|
|
1600
|
+
events.push({
|
|
1601
|
+
name: "lead",
|
|
1602
|
+
description: "Lead event for Meta CAPI",
|
|
1603
|
+
properties: ["email", "phone", "fbc", "fbp"],
|
|
1604
|
+
serverSide: true
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
if (config.adPlatforms.includes("tiktok")) {
|
|
1608
|
+
events.push({
|
|
1609
|
+
name: "complete_payment",
|
|
1610
|
+
description: "Purchase event for TikTok Events API",
|
|
1611
|
+
properties: ["value", "currency", "email", "phone", "ttclid"],
|
|
1612
|
+
serverSide: true
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
if (config.adPlatforms.includes("google")) {
|
|
1616
|
+
events.push({
|
|
1617
|
+
name: "conversion",
|
|
1618
|
+
description: "Conversion event for Google Ads",
|
|
1619
|
+
properties: ["value", "currency", "email", "phone", "gclid"],
|
|
1620
|
+
serverSide: true
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
return events;
|
|
1625
|
+
}
|
|
1626
|
+
function getPlatformPostInstallSteps(config) {
|
|
1627
|
+
const steps = [];
|
|
1628
|
+
if (config.platformType === "mobile" || config.platformType === "both") {
|
|
1629
|
+
steps.push("Run `cd ios && pod install` to install native dependencies");
|
|
1630
|
+
if (config.enableDeepLinking) {
|
|
1631
|
+
steps.push("Configure URL schemes in Xcode/Android manifest for deep linking");
|
|
1632
|
+
steps.push("Set up Associated Domains (iOS) or App Links (Android)");
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
for (const platformId of config.adPlatforms) {
|
|
1636
|
+
const platform = AD_PLATFORMS.find((p2) => p2.id === platformId);
|
|
1637
|
+
if (!platform || platformId === "none") continue;
|
|
1638
|
+
steps.push(`Add ${platform.label} credentials to your environment variables`);
|
|
1639
|
+
if (platform.requiresServerSide) {
|
|
1640
|
+
steps.push(`Set up server-side endpoint for ${platform.label} conversions`);
|
|
1641
|
+
}
|
|
1642
|
+
if (config.platformType !== "web") {
|
|
1643
|
+
if (platformId === "meta") {
|
|
1644
|
+
steps.push("Add Meta App ID to Info.plist (iOS) and AndroidManifest.xml");
|
|
1645
|
+
}
|
|
1646
|
+
if (platformId === "tiktok") {
|
|
1647
|
+
steps.push("Add TikTok App ID to Info.plist (iOS) and AndroidManifest.xml");
|
|
1648
|
+
}
|
|
1649
|
+
if (platformId === "apple_search_ads") {
|
|
1650
|
+
steps.push("Enable AdServices.framework in Xcode");
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return steps;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/generators/ai-context.ts
|
|
1658
|
+
function inferPropertyType(propName) {
|
|
1659
|
+
const lowerName = propName.toLowerCase();
|
|
1660
|
+
if (["value", "price", "amount", "total", "quantity", "count", "steps_completed"].some((n) => lowerName.includes(n))) {
|
|
1661
|
+
return "number";
|
|
1662
|
+
}
|
|
1663
|
+
if (["is_", "has_", "enable", "disable"].some((n) => lowerName.startsWith(n) || lowerName.includes(n))) {
|
|
1664
|
+
return "boolean";
|
|
1665
|
+
}
|
|
1666
|
+
if (lowerName === "currency") {
|
|
1667
|
+
return 'string; // ISO 4217 code (e.g., "USD", "EUR")';
|
|
1668
|
+
}
|
|
1669
|
+
if (lowerName === "email") {
|
|
1670
|
+
return "string; // User email for attribution matching";
|
|
1671
|
+
}
|
|
1672
|
+
if (lowerName === "phone") {
|
|
1673
|
+
return "string; // User phone for attribution matching";
|
|
1674
|
+
}
|
|
1675
|
+
if (lowerName.endsWith("_id") || lowerName === "id") {
|
|
1676
|
+
return "string";
|
|
1677
|
+
}
|
|
1678
|
+
return "string";
|
|
1679
|
+
}
|
|
1680
|
+
function generateAIContextDoc(params) {
|
|
1681
|
+
const {
|
|
1682
|
+
workspaceName,
|
|
1683
|
+
workspaceId,
|
|
1684
|
+
framework,
|
|
1685
|
+
platformType,
|
|
1686
|
+
adPlatforms,
|
|
1687
|
+
businessType,
|
|
1688
|
+
selectedEvents,
|
|
1689
|
+
sdks,
|
|
1690
|
+
enableServerSideConversions,
|
|
1691
|
+
enableContainer = true
|
|
1692
|
+
} = params;
|
|
1693
|
+
const eventsList = selectedEvents.map((e) => {
|
|
1694
|
+
const propsWithTypes = e.properties.map((p2) => {
|
|
1695
|
+
const type = inferPropertyType(p2);
|
|
1696
|
+
return ` ${p2}: ${type};`;
|
|
1697
|
+
}).join("\n");
|
|
1698
|
+
return `### \`${e.name}\`
|
|
1699
|
+
${e.description}
|
|
1700
|
+
|
|
1701
|
+
\`\`\`typescript
|
|
1702
|
+
datalyr.track('${e.name}', {
|
|
1703
|
+
${propsWithTypes}
|
|
1704
|
+
});
|
|
1705
|
+
\`\`\``;
|
|
1706
|
+
}).join("\n\n");
|
|
1707
|
+
const trackingExamples = selectedEvents.slice(0, 3).map((e) => {
|
|
1708
|
+
const props = e.properties.slice(0, 2).map((p2) => `${p2}: '...'`).join(", ");
|
|
1709
|
+
return `datalyr.track('${e.name}', { ${props} });`;
|
|
1710
|
+
}).join("\n");
|
|
1711
|
+
const serverSideExample = enableServerSideConversions ? `
|
|
1712
|
+
## Server-Side Tracking (CAPI)
|
|
1713
|
+
|
|
1714
|
+
For conversion events that need server-side tracking:
|
|
1715
|
+
|
|
1716
|
+
\`\`\`typescript
|
|
1717
|
+
// In your API route or server action
|
|
1718
|
+
import { datalyr } from '@/lib/datalyr.server';
|
|
1719
|
+
|
|
1720
|
+
await datalyr.track({
|
|
1721
|
+
event: 'purchase',
|
|
1722
|
+
userId: user.id,
|
|
1723
|
+
properties: {
|
|
1724
|
+
value: 99.00,
|
|
1725
|
+
currency: 'USD',
|
|
1726
|
+
email: user.email, // Required for ad platform matching
|
|
1727
|
+
phone: user.phone, // Optional, improves match rate
|
|
1728
|
+
},
|
|
1729
|
+
});
|
|
1730
|
+
\`\`\`
|
|
1731
|
+
` : "";
|
|
1732
|
+
const adPlatformsSection = adPlatforms.length > 0 ? `
|
|
1733
|
+
## Attribution Setup
|
|
1734
|
+
|
|
1735
|
+
Ad platforms configured: ${adPlatforms.join(", ")}
|
|
1736
|
+
|
|
1737
|
+
For proper attribution matching, include user data in conversion events:
|
|
1738
|
+
- \`email\`: User's email (hashed automatically)
|
|
1739
|
+
- \`phone\`: User's phone number (hashed automatically)
|
|
1740
|
+
${adPlatforms.includes("meta") ? "- `fbc`, `fbp`: Meta click/browser IDs (from cookies)" : ""}
|
|
1741
|
+
${adPlatforms.includes("google") ? "- `gclid`: Google click ID (from URL params)" : ""}
|
|
1742
|
+
${adPlatforms.includes("tiktok") ? "- `ttclid`: TikTok click ID (from URL params)" : ""}
|
|
1743
|
+
` : "";
|
|
1744
|
+
const containerSection = enableContainer ? `
|
|
1745
|
+
## Container Scripts
|
|
1746
|
+
|
|
1747
|
+
Container scripts are **enabled**. Third-party pixels (Meta Pixel, Google Tag, TikTok Pixel)
|
|
1748
|
+
are managed through the Datalyr dashboard, not in code.
|
|
1749
|
+
|
|
1750
|
+
Configure pixels at: https://app.datalyr.com/dashboard/${workspaceId}/settings/pixels
|
|
1751
|
+
|
|
1752
|
+
Benefits:
|
|
1753
|
+
- Add/remove pixels without code changes
|
|
1754
|
+
- Events tracked with \`datalyr.track()\` auto-fire to all configured pixels
|
|
1755
|
+
- Server-side fallback for ad blockers
|
|
1756
|
+
` : "";
|
|
1757
|
+
return `# Datalyr Analytics Setup
|
|
1758
|
+
|
|
1759
|
+
This file documents the Datalyr analytics configuration for AI coding assistants.
|
|
1760
|
+
|
|
1761
|
+
## Project Info
|
|
1762
|
+
|
|
1763
|
+
- **Workspace**: ${workspaceName}
|
|
1764
|
+
- **Workspace ID**: \`${workspaceId}\`
|
|
1765
|
+
- **Framework**: ${framework}
|
|
1766
|
+
- **Platform**: ${platformType}
|
|
1767
|
+
- **Business Type**: ${businessType}
|
|
1768
|
+
- **SDKs**: ${sdks.join(", ")}
|
|
1769
|
+
|
|
1770
|
+
## Quick Start
|
|
1771
|
+
|
|
1772
|
+
${framework === "ios" ? `\`\`\`swift
|
|
1773
|
+
import DatalyrSDK
|
|
1774
|
+
|
|
1775
|
+
// Track an event
|
|
1776
|
+
Datalyr.shared.track("event_name", properties: ["key": "value"])
|
|
1777
|
+
\`\`\`` : `\`\`\`typescript
|
|
1778
|
+
import datalyr from '@datalyr/web';
|
|
1779
|
+
|
|
1780
|
+
// Track an event
|
|
1781
|
+
${trackingExamples}
|
|
1782
|
+
\`\`\``}
|
|
1783
|
+
|
|
1784
|
+
## Events to Track
|
|
1785
|
+
|
|
1786
|
+
${eventsList}
|
|
1787
|
+
|
|
1788
|
+
## Usage Patterns
|
|
1789
|
+
|
|
1790
|
+
### Identify Users
|
|
1791
|
+
|
|
1792
|
+
\`\`\`typescript
|
|
1793
|
+
// After user signs in
|
|
1794
|
+
datalyr.identify(user.id, {
|
|
1795
|
+
email: user.email,
|
|
1796
|
+
name: user.name,
|
|
1797
|
+
plan: user.plan,
|
|
1798
|
+
});
|
|
1799
|
+
\`\`\`
|
|
1800
|
+
|
|
1801
|
+
### Track Events
|
|
1802
|
+
|
|
1803
|
+
\`\`\`typescript
|
|
1804
|
+
// Track any event with properties
|
|
1805
|
+
datalyr.track('event_name', {
|
|
1806
|
+
property: 'value',
|
|
1807
|
+
value: 100,
|
|
1808
|
+
});
|
|
1809
|
+
\`\`\`
|
|
1810
|
+
${serverSideExample}${adPlatformsSection}${containerSection}
|
|
1811
|
+
## Dashboard
|
|
1812
|
+
|
|
1813
|
+
View your analytics at:
|
|
1814
|
+
https://app.datalyr.com/dashboard/${workspaceId}/events
|
|
1815
|
+
|
|
1816
|
+
## Documentation
|
|
1817
|
+
|
|
1818
|
+
- SDK Docs: https://docs.datalyr.com/sdks/${framework}
|
|
1819
|
+
- API Reference: https://docs.datalyr.com/api
|
|
1820
|
+
`;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// src/agent/runner.ts
|
|
1824
|
+
var import_promises = require("fs/promises");
|
|
1825
|
+
var import_path3 = require("path");
|
|
1826
|
+
var import_fs2 = require("fs");
|
|
1827
|
+
var import_child_process = require("child_process");
|
|
1828
|
+
var import_util = require("util");
|
|
1829
|
+
var import_glob2 = require("glob");
|
|
1830
|
+
var execAsync = (0, import_util.promisify)(import_child_process.exec);
|
|
1831
|
+
var LLM_GATEWAY_URL = process.env.DATALYR_LLM_GATEWAY || "https://wizard.datalyr.com";
|
|
1832
|
+
async function runAgentWizard(_config, options = {}) {
|
|
1833
|
+
const cwd = options.cwd || process.cwd();
|
|
1834
|
+
p.intro(import_chalk.default.cyan("Datalyr AI Wizard"));
|
|
1835
|
+
const useAI = await p.confirm({
|
|
1836
|
+
message: "This wizard uses AI to analyze your project and install Datalyr. Continue?",
|
|
1837
|
+
initialValue: true
|
|
1838
|
+
});
|
|
1839
|
+
if (p.isCancel(useAI) || !useAI) {
|
|
1840
|
+
p.cancel("Wizard cancelled");
|
|
1841
|
+
return { success: false, error: "User cancelled" };
|
|
1842
|
+
}
|
|
1843
|
+
let apiKey = options.apiKey;
|
|
1844
|
+
let workspace = null;
|
|
1845
|
+
let keyAttempts = 0;
|
|
1846
|
+
const maxAttempts = 3;
|
|
1847
|
+
while (!workspace && keyAttempts < maxAttempts) {
|
|
1848
|
+
keyAttempts++;
|
|
1849
|
+
if (!apiKey) {
|
|
1850
|
+
if (keyAttempts === 1) {
|
|
1851
|
+
p.note(
|
|
1852
|
+
`To get your API key:
|
|
1853
|
+
1. Go to ${import_chalk.default.cyan("https://app.datalyr.com/settings/api")}
|
|
1854
|
+
2. Create a new API key (or copy existing)
|
|
1855
|
+
3. Paste it below`,
|
|
1856
|
+
"API Key Required"
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
const keyInput = await p.text({
|
|
1860
|
+
message: "Enter your Datalyr API key (starts with dk_):",
|
|
1861
|
+
placeholder: "dk_live_...",
|
|
1862
|
+
validate: (value) => {
|
|
1863
|
+
if (!value) return "API key is required";
|
|
1864
|
+
if (!value.startsWith("dk_")) return "API key must start with dk_";
|
|
1865
|
+
if (value.length < 20) return "API key seems too short";
|
|
1866
|
+
return void 0;
|
|
1867
|
+
}
|
|
1868
|
+
});
|
|
1869
|
+
if (p.isCancel(keyInput)) {
|
|
1870
|
+
p.cancel("Wizard cancelled");
|
|
1871
|
+
return { success: false, error: "User cancelled" };
|
|
1872
|
+
}
|
|
1873
|
+
apiKey = keyInput;
|
|
1874
|
+
}
|
|
1875
|
+
const validateSpinner = p.spinner();
|
|
1876
|
+
validateSpinner.start("Validating API key...");
|
|
1877
|
+
try {
|
|
1878
|
+
const defaultWorkspace = await validateApiKey(apiKey);
|
|
1879
|
+
const allWorkspaces = await fetchWorkspaces(apiKey);
|
|
1880
|
+
if (allWorkspaces.length > 1) {
|
|
1881
|
+
validateSpinner.stop("API key validated");
|
|
1882
|
+
const workspaceChoice = await p.select({
|
|
1883
|
+
message: "Select a workspace to configure:",
|
|
1884
|
+
options: allWorkspaces.map((w) => ({
|
|
1885
|
+
value: w.id,
|
|
1886
|
+
label: w.name,
|
|
1887
|
+
hint: w.domain || void 0
|
|
1888
|
+
}))
|
|
1889
|
+
});
|
|
1890
|
+
if (p.isCancel(workspaceChoice)) {
|
|
1891
|
+
p.cancel("Wizard cancelled");
|
|
1892
|
+
return { success: false, error: "User cancelled" };
|
|
1893
|
+
}
|
|
1894
|
+
const selectedWorkspace = allWorkspaces.find((w) => w.id === workspaceChoice);
|
|
1895
|
+
if (selectedWorkspace) {
|
|
1896
|
+
workspace = {
|
|
1897
|
+
id: selectedWorkspace.id,
|
|
1898
|
+
name: selectedWorkspace.name,
|
|
1899
|
+
timezone: null,
|
|
1900
|
+
domain: selectedWorkspace.domain
|
|
1901
|
+
};
|
|
1902
|
+
} else {
|
|
1903
|
+
workspace = defaultWorkspace;
|
|
1904
|
+
}
|
|
1905
|
+
p.log.info(`Selected workspace: ${import_chalk.default.cyan(workspace.name)}`);
|
|
1906
|
+
} else {
|
|
1907
|
+
workspace = defaultWorkspace;
|
|
1908
|
+
validateSpinner.stop(`Workspace: ${import_chalk.default.cyan(workspace.name)}`);
|
|
1909
|
+
}
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
validateSpinner.stop(import_chalk.default.red("Invalid API key"));
|
|
1912
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to validate API key";
|
|
1913
|
+
p.log.error(errorMessage);
|
|
1914
|
+
apiKey = void 0;
|
|
1915
|
+
if (keyAttempts < maxAttempts) {
|
|
1916
|
+
const action = await p.select({
|
|
1917
|
+
message: "What would you like to do?",
|
|
1918
|
+
options: [
|
|
1919
|
+
{ value: "retry", label: "Try a different API key" },
|
|
1920
|
+
{ value: "signup", label: "Create a free account", hint: "Opens browser" },
|
|
1921
|
+
{ value: "exit", label: "Exit wizard" }
|
|
1922
|
+
]
|
|
1923
|
+
});
|
|
1924
|
+
if (p.isCancel(action) || action === "exit") {
|
|
1925
|
+
p.cancel("Wizard cancelled");
|
|
1926
|
+
return { success: false, error: "User cancelled" };
|
|
1927
|
+
}
|
|
1928
|
+
if (action === "signup") {
|
|
1929
|
+
const signupUrl = "https://app.datalyr.com/signup?ref=wizard";
|
|
1930
|
+
p.log.info(`Opening ${import_chalk.default.cyan(signupUrl)} in your browser...`);
|
|
1931
|
+
try {
|
|
1932
|
+
const { exec: exec2 } = await import("child_process");
|
|
1933
|
+
const { promisify: promisify2 } = await import("util");
|
|
1934
|
+
const execAsync2 = promisify2(exec2);
|
|
1935
|
+
const platform = process.platform;
|
|
1936
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
1937
|
+
await execAsync2(`${cmd} "${signupUrl}"`);
|
|
1938
|
+
p.log.success("Browser opened! Create your account and come back with your API key.");
|
|
1939
|
+
await p.text({
|
|
1940
|
+
message: "Press Enter when you have your API key ready..."
|
|
1941
|
+
});
|
|
1942
|
+
} catch {
|
|
1943
|
+
p.log.warn(`Please visit: ${import_chalk.default.cyan(signupUrl)}`);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
if (!workspace || !apiKey) {
|
|
1950
|
+
p.log.error(`Failed to validate API key after ${maxAttempts} attempts.`);
|
|
1951
|
+
p.log.info(`Get help at ${import_chalk.default.cyan("https://docs.datalyr.com/getting-started")}`);
|
|
1952
|
+
return { success: false, error: "API key validation failed" };
|
|
1953
|
+
}
|
|
1954
|
+
const validatedApiKey = apiKey;
|
|
1955
|
+
const detectSpinner = p.spinner();
|
|
1956
|
+
detectSpinner.start("Analyzing your project...");
|
|
1957
|
+
let detection = await detectFramework(cwd);
|
|
1958
|
+
let framework = detection.framework;
|
|
1959
|
+
if (framework === "unknown" && !options.framework) {
|
|
1960
|
+
detectSpinner.stop("Could not auto-detect framework");
|
|
1961
|
+
const frameworkChoice = await p.select({
|
|
1962
|
+
message: "Select your framework:",
|
|
1963
|
+
options: [
|
|
1964
|
+
{ value: "nextjs", label: "Next.js" },
|
|
1965
|
+
{ value: "react", label: "React" },
|
|
1966
|
+
{ value: "react-vite", label: "React (Vite)" },
|
|
1967
|
+
{ value: "svelte", label: "Svelte" },
|
|
1968
|
+
{ value: "sveltekit", label: "SvelteKit" },
|
|
1969
|
+
{ value: "react-native", label: "React Native" },
|
|
1970
|
+
{ value: "expo", label: "Expo" },
|
|
1971
|
+
{ value: "ios", label: "iOS (Swift)" },
|
|
1972
|
+
{ value: "node", label: "Node.js" }
|
|
1973
|
+
]
|
|
1974
|
+
});
|
|
1975
|
+
if (p.isCancel(frameworkChoice)) {
|
|
1976
|
+
p.cancel("Wizard cancelled");
|
|
1977
|
+
return { success: false, error: "User cancelled" };
|
|
1978
|
+
}
|
|
1979
|
+
framework = frameworkChoice;
|
|
1980
|
+
} else if (options.framework) {
|
|
1981
|
+
framework = options.framework;
|
|
1982
|
+
}
|
|
1983
|
+
detectSpinner.stop(`Detected: ${import_chalk.default.cyan(getFrameworkDisplayName(framework))}`);
|
|
1984
|
+
if (detection.framework !== framework) {
|
|
1985
|
+
detection = { ...detection, framework, sdks: getSdksForFramework(framework) };
|
|
1986
|
+
}
|
|
1987
|
+
const isMobileFramework = ["react-native", "expo", "ios"].includes(framework);
|
|
1988
|
+
const isWebFramework = ["nextjs", "react", "react-vite", "svelte", "sveltekit"].includes(framework);
|
|
1989
|
+
let platformType = "web";
|
|
1990
|
+
if (isMobileFramework) {
|
|
1991
|
+
platformType = "mobile";
|
|
1992
|
+
} else if (!isMobileFramework && !isWebFramework) {
|
|
1993
|
+
const platformChoice = await p.select({
|
|
1994
|
+
message: "What platforms are you targeting?",
|
|
1995
|
+
options: PLATFORM_TYPES.map((type) => ({
|
|
1996
|
+
value: type.id,
|
|
1997
|
+
label: type.label,
|
|
1998
|
+
hint: type.hint
|
|
1999
|
+
}))
|
|
2000
|
+
});
|
|
2001
|
+
if (p.isCancel(platformChoice)) {
|
|
2002
|
+
p.cancel("Wizard cancelled");
|
|
2003
|
+
return { success: false, error: "User cancelled" };
|
|
2004
|
+
}
|
|
2005
|
+
platformType = platformChoice;
|
|
2006
|
+
}
|
|
2007
|
+
const runningAds = await p.confirm({
|
|
2008
|
+
message: "Are you running paid ads (Meta, Google, TikTok)?",
|
|
2009
|
+
initialValue: false
|
|
2010
|
+
});
|
|
2011
|
+
let adPlatforms = [];
|
|
2012
|
+
let platformConfig = {
|
|
2013
|
+
platformType,
|
|
2014
|
+
adPlatforms: [],
|
|
2015
|
+
enableDeepLinking: false,
|
|
2016
|
+
enableServerSideConversions: false
|
|
2017
|
+
};
|
|
2018
|
+
if (!p.isCancel(runningAds) && runningAds) {
|
|
2019
|
+
const adPlatformChoices = await p.multiselect({
|
|
2020
|
+
message: "Select your ad platforms:",
|
|
2021
|
+
options: AD_PLATFORMS.filter((p2) => p2.id !== "none").map((platform) => ({
|
|
2022
|
+
value: platform.id,
|
|
2023
|
+
label: platform.label,
|
|
2024
|
+
hint: platform.description
|
|
2025
|
+
})),
|
|
2026
|
+
required: false
|
|
2027
|
+
});
|
|
2028
|
+
if (!p.isCancel(adPlatformChoices)) {
|
|
2029
|
+
adPlatforms = adPlatformChoices;
|
|
2030
|
+
if (adPlatforms.length > 0) {
|
|
2031
|
+
const serverSide = await p.confirm({
|
|
2032
|
+
message: "Enable server-side conversion tracking (CAPI)?",
|
|
2033
|
+
initialValue: true
|
|
2034
|
+
});
|
|
2035
|
+
platformConfig.enableServerSideConversions = !p.isCancel(serverSide) && serverSide;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
if (platformType === "mobile" || platformType === "both") {
|
|
2040
|
+
const deepLinking = await p.confirm({
|
|
2041
|
+
message: "Set up deep linking / deferred deep links?",
|
|
2042
|
+
initialValue: adPlatforms.length > 0
|
|
2043
|
+
});
|
|
2044
|
+
platformConfig.enableDeepLinking = !p.isCancel(deepLinking) && deepLinking;
|
|
2045
|
+
}
|
|
2046
|
+
platformConfig.adPlatforms = adPlatforms;
|
|
2047
|
+
let enableContainer = options.enableContainer;
|
|
2048
|
+
if (enableContainer === void 0 && platformType !== "mobile") {
|
|
2049
|
+
const containerChoice = await p.confirm({
|
|
2050
|
+
message: "Manage third-party pixels through Datalyr dashboard?",
|
|
2051
|
+
initialValue: adPlatforms.length > 0
|
|
2052
|
+
// Default yes if using ad platforms
|
|
2053
|
+
});
|
|
2054
|
+
if (!p.isCancel(containerChoice)) {
|
|
2055
|
+
enableContainer = containerChoice;
|
|
2056
|
+
if (containerChoice) {
|
|
2057
|
+
p.note(
|
|
2058
|
+
`Container scripts let you:
|
|
2059
|
+
${import_chalk.default.green("\u2022")} Add/remove Meta, Google, TikTok pixels without code
|
|
2060
|
+
${import_chalk.default.green("\u2022")} Auto-fire datalyr.track() events to all pixels
|
|
2061
|
+
${import_chalk.default.green("\u2022")} Inject custom scripts (Hotjar, Intercom, etc.)`,
|
|
2062
|
+
"Container Scripts"
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
const containerEnabled = enableContainer !== false;
|
|
2068
|
+
const businessTypeChoice = await p.select({
|
|
2069
|
+
message: "What type of app are you building?",
|
|
2070
|
+
options: BUSINESS_TYPES.map((type) => ({
|
|
2071
|
+
value: type.id,
|
|
2072
|
+
label: type.label,
|
|
2073
|
+
hint: type.hint
|
|
2074
|
+
}))
|
|
2075
|
+
});
|
|
2076
|
+
if (p.isCancel(businessTypeChoice)) {
|
|
2077
|
+
p.cancel("Wizard cancelled");
|
|
2078
|
+
return { success: false, error: "User cancelled" };
|
|
2079
|
+
}
|
|
2080
|
+
const selectedBusinessType = businessTypeChoice;
|
|
2081
|
+
const suggestedEvents = getEventSuggestions(selectedBusinessType);
|
|
2082
|
+
const attributionEvents = getAttributionEvents(platformConfig);
|
|
2083
|
+
const autoTrackedEvents = platformType === "mobile" || platformType === "both" ? ["page_view / screen_view", "session_start", "app_install (mobile)", "attribution data"] : ["page_view", "session_start", "referrer", "UTM parameters"];
|
|
2084
|
+
p.note(
|
|
2085
|
+
`${import_chalk.default.bold("Auto-tracked (no code needed):")}
|
|
2086
|
+
` + autoTrackedEvents.map((e) => ` ${import_chalk.default.dim("\u2022")} ${e}`).join("\n") + `
|
|
2087
|
+
|
|
2088
|
+
${import_chalk.default.bold("High-value events")} are the key actions that matter for your business:
|
|
2089
|
+
${import_chalk.default.dim("\u2022")} Conversions (signups, purchases, leads)
|
|
2090
|
+
${import_chalk.default.dim("\u2022")} Engagement (feature usage, key interactions)
|
|
2091
|
+
${import_chalk.default.dim("\u2022")} Revenue (transactions, subscriptions)`,
|
|
2092
|
+
"Event Tracking"
|
|
2093
|
+
);
|
|
2094
|
+
const allEvents = [...suggestedEvents, ...attributionEvents.map((e) => ({
|
|
2095
|
+
name: e.name,
|
|
2096
|
+
description: e.description,
|
|
2097
|
+
properties: e.properties,
|
|
2098
|
+
priority: "high"
|
|
2099
|
+
}))];
|
|
2100
|
+
const uniqueEvents = allEvents.filter(
|
|
2101
|
+
(event, index, self) => index === self.findIndex((e) => e.name === event.name)
|
|
2102
|
+
);
|
|
2103
|
+
const eventOptions = buildEventSelectOptions(uniqueEvents, true);
|
|
2104
|
+
const businessTypeLabel = BUSINESS_TYPES.find((b) => b.id === selectedBusinessType)?.label || selectedBusinessType;
|
|
2105
|
+
p.note(
|
|
2106
|
+
`Based on your ${import_chalk.default.cyan(businessTypeLabel)} app, we suggest these events:
|
|
2107
|
+
|
|
2108
|
+
` + uniqueEvents.filter((e) => e.priority === "high").map((e) => ` ${import_chalk.default.green("\u2022")} ${import_chalk.default.bold(e.name)}: ${formatEventDescription(e)}`).join("\n"),
|
|
2109
|
+
"Suggested Events"
|
|
2110
|
+
);
|
|
2111
|
+
const selectedEventNames = await p.multiselect({
|
|
2112
|
+
message: "Select events to track:",
|
|
2113
|
+
options: eventOptions,
|
|
2114
|
+
initialValues: ["__all_recommended__"],
|
|
2115
|
+
required: false
|
|
2116
|
+
});
|
|
2117
|
+
let selectedEvents = p.isCancel(selectedEventNames) ? uniqueEvents.filter((e) => e.priority === "high") : resolveSelectedEvents(selectedEventNames, uniqueEvents);
|
|
2118
|
+
const addCustom = await p.confirm({
|
|
2119
|
+
message: "Want to add any custom events specific to your app?",
|
|
2120
|
+
initialValue: false
|
|
2121
|
+
});
|
|
2122
|
+
if (!p.isCancel(addCustom) && addCustom) {
|
|
2123
|
+
const customEventInput = await p.text({
|
|
2124
|
+
message: "Enter custom event names (comma-separated):",
|
|
2125
|
+
placeholder: "checkout_completed, feature_clicked, ..."
|
|
2126
|
+
});
|
|
2127
|
+
if (!p.isCancel(customEventInput) && customEventInput) {
|
|
2128
|
+
const customEvents = customEventInput.split(",").map((e) => e.trim()).filter((e) => e.length > 0).map((name) => ({
|
|
2129
|
+
name,
|
|
2130
|
+
description: "Custom event",
|
|
2131
|
+
properties: ["value", "context"],
|
|
2132
|
+
priority: "high"
|
|
2133
|
+
}));
|
|
2134
|
+
selectedEvents = [...selectedEvents, ...customEvents];
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
if (selectedEvents.length > 0) {
|
|
2138
|
+
const eventList = selectedEvents.slice(0, 6).map((e) => ` ${import_chalk.default.green("\u2022")} ${import_chalk.default.bold(e.name)}`).join("\n");
|
|
2139
|
+
const moreCount = selectedEvents.length > 6 ? ` (+${selectedEvents.length - 6} more)` : "";
|
|
2140
|
+
p.note(
|
|
2141
|
+
`Events you'll track:
|
|
2142
|
+
|
|
2143
|
+
${eventList}${moreCount}`,
|
|
2144
|
+
"Your Events"
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
const platformSdks = getSdksForPlatform(platformType, adPlatforms);
|
|
2148
|
+
const allSdks = [.../* @__PURE__ */ new Set([...detection.sdks, ...platformSdks])];
|
|
2149
|
+
const platformSteps = getPlatformPostInstallSteps(platformConfig);
|
|
2150
|
+
p.note(
|
|
2151
|
+
`The wizard will:
|
|
2152
|
+
${import_chalk.default.green("\u2022")} Install ${import_chalk.default.cyan(allSdks.join(", "))}
|
|
2153
|
+
${import_chalk.default.green("\u2022")} Create initialization code
|
|
2154
|
+
${import_chalk.default.green("\u2022")} Configure environment variables
|
|
2155
|
+
${import_chalk.default.green("\u2022")} Set up workspace: ${import_chalk.default.cyan(workspace.name)}
|
|
2156
|
+
${adPlatforms.length > 0 ? `${import_chalk.default.green("\u2022")} Configure attribution for: ${import_chalk.default.cyan(adPlatforms.join(", "))}` : ""}
|
|
2157
|
+
${containerEnabled && platformType !== "mobile" ? `${import_chalk.default.green("\u2022")} Enable container scripts for pixel management` : ""}`,
|
|
2158
|
+
"Installation Plan"
|
|
2159
|
+
);
|
|
2160
|
+
const proceed = await p.confirm({
|
|
2161
|
+
message: "Proceed with installation?",
|
|
2162
|
+
initialValue: true
|
|
2163
|
+
});
|
|
2164
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
2165
|
+
p.cancel("Wizard cancelled");
|
|
2166
|
+
return { success: false, error: "User cancelled" };
|
|
2167
|
+
}
|
|
2168
|
+
const agentSpinner = p.spinner();
|
|
2169
|
+
agentSpinner.start("AI agent is working...");
|
|
2170
|
+
try {
|
|
2171
|
+
const result = await executeAgent({
|
|
2172
|
+
framework,
|
|
2173
|
+
apiKey: validatedApiKey,
|
|
2174
|
+
cwd,
|
|
2175
|
+
docs: getFrameworkDocs(framework, validatedApiKey),
|
|
2176
|
+
debug: options.debug,
|
|
2177
|
+
enableContainer: containerEnabled,
|
|
2178
|
+
workspaceId: workspace.id
|
|
2179
|
+
});
|
|
2180
|
+
if (result.success) {
|
|
2181
|
+
agentSpinner.stop(import_chalk.default.green("Installation complete!"));
|
|
2182
|
+
if (!options.skipVerification) {
|
|
2183
|
+
const verifyResult = await verifyInstallation(validatedApiKey, workspace.id);
|
|
2184
|
+
if (verifyResult.success) {
|
|
2185
|
+
p.log.success("SDK verified and ready to track events!");
|
|
2186
|
+
} else {
|
|
2187
|
+
p.log.warn("Could not verify SDK installation. This may be normal if events haven't been sent yet.");
|
|
2188
|
+
p.log.info(`Check your dashboard at: ${import_chalk.default.cyan(`https://app.datalyr.com/dashboard/${workspace.id}/events`)}`);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
try {
|
|
2192
|
+
const aiContextDoc = generateAIContextDoc({
|
|
2193
|
+
workspaceName: workspace.name,
|
|
2194
|
+
workspaceId: workspace.id,
|
|
2195
|
+
framework,
|
|
2196
|
+
platformType,
|
|
2197
|
+
adPlatforms,
|
|
2198
|
+
businessType: selectedBusinessType,
|
|
2199
|
+
selectedEvents,
|
|
2200
|
+
sdks: allSdks,
|
|
2201
|
+
enableServerSideConversions: platformConfig.enableServerSideConversions,
|
|
2202
|
+
enableContainer: containerEnabled
|
|
2203
|
+
});
|
|
2204
|
+
await (0, import_promises.writeFile)((0, import_path3.join)(cwd, ".datalyr.md"), aiContextDoc);
|
|
2205
|
+
p.log.success("Created .datalyr.md for AI coding assistants");
|
|
2206
|
+
} catch {
|
|
2207
|
+
}
|
|
2208
|
+
const eventExamples = selectedEvents.slice(0, 3).map(
|
|
2209
|
+
(e) => `datalyr.track('${e.name}', { ${e.properties.slice(0, 2).map((p2) => `${p2}: '...'`).join(", ")} })`
|
|
2210
|
+
).join("\n ");
|
|
2211
|
+
const stepsText = platformSteps.length > 0 ? `
|
|
2212
|
+
|
|
2213
|
+
${import_chalk.default.bold("Next steps:")}
|
|
2214
|
+
${platformSteps.map((s) => ` ${import_chalk.default.yellow("\u2192")} ${s}`).join("\n")}` : "";
|
|
2215
|
+
p.note(
|
|
2216
|
+
`Your workspace "${workspace.name}" is ready!
|
|
2217
|
+
|
|
2218
|
+
${import_chalk.default.bold("Start tracking events:")}
|
|
2219
|
+
${eventExamples || "datalyr.track('event_name', { property: 'value' })"}
|
|
2220
|
+
|
|
2221
|
+
${import_chalk.default.bold("View your data:")}
|
|
2222
|
+
${import_chalk.default.cyan(`https://app.datalyr.com/dashboard/${workspace.id}/events`)}${stepsText}`,
|
|
2223
|
+
"Success!"
|
|
2224
|
+
);
|
|
2225
|
+
p.outro(import_chalk.default.green("Datalyr is ready to use!"));
|
|
2226
|
+
} else {
|
|
2227
|
+
agentSpinner.stop(import_chalk.default.red("Installation failed"));
|
|
2228
|
+
p.log.error(result.error || "Unknown error");
|
|
2229
|
+
}
|
|
2230
|
+
return result;
|
|
2231
|
+
} catch (error) {
|
|
2232
|
+
agentSpinner.stop(import_chalk.default.red("Agent error"));
|
|
2233
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2234
|
+
p.log.error(errorMessage);
|
|
2235
|
+
return { success: false, error: errorMessage };
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
async function executeAgent(params) {
|
|
2239
|
+
const { framework, apiKey, cwd, docs, debug, enableContainer = true, workspaceId } = params;
|
|
2240
|
+
const initialMessage = buildIntegrationPrompt(framework, apiKey, docs, enableContainer, workspaceId);
|
|
2241
|
+
const messages = [
|
|
2242
|
+
{ role: "user", content: initialMessage }
|
|
2243
|
+
];
|
|
2244
|
+
const filesModified = [];
|
|
2245
|
+
const maxIterations = 20;
|
|
2246
|
+
let iterations = 0;
|
|
2247
|
+
while (iterations < maxIterations) {
|
|
2248
|
+
iterations++;
|
|
2249
|
+
if (debug) {
|
|
2250
|
+
console.log(`[DEBUG] Agent iteration ${iterations}`);
|
|
2251
|
+
}
|
|
2252
|
+
try {
|
|
2253
|
+
const controller = new AbortController();
|
|
2254
|
+
const timeoutId = setTimeout(() => controller.abort(), 6e4);
|
|
2255
|
+
const response = await fetch(`${LLM_GATEWAY_URL}/agent`, {
|
|
2256
|
+
method: "POST",
|
|
2257
|
+
headers: {
|
|
2258
|
+
"Content-Type": "application/json"
|
|
2259
|
+
},
|
|
2260
|
+
body: JSON.stringify({
|
|
2261
|
+
messages,
|
|
2262
|
+
system: buildSystemPrompt()
|
|
2263
|
+
}),
|
|
2264
|
+
signal: controller.signal
|
|
2265
|
+
});
|
|
2266
|
+
clearTimeout(timeoutId);
|
|
2267
|
+
if (!response.ok) {
|
|
2268
|
+
const error = await response.text();
|
|
2269
|
+
return { success: false, error: `Gateway error: ${error}` };
|
|
2270
|
+
}
|
|
2271
|
+
const claudeResponse = await response.json();
|
|
2272
|
+
if (debug) {
|
|
2273
|
+
console.log("[DEBUG] Response:", JSON.stringify(claudeResponse, null, 2));
|
|
2274
|
+
}
|
|
2275
|
+
messages.push({ role: "assistant", content: claudeResponse.content });
|
|
2276
|
+
if (claudeResponse.stop_reason === "end_turn") {
|
|
2277
|
+
const taskComplete = claudeResponse.content.find(
|
|
2278
|
+
(block) => block.type === "tool_use" && block.name === "task_complete"
|
|
2279
|
+
);
|
|
2280
|
+
if (taskComplete) {
|
|
2281
|
+
const input = taskComplete.input;
|
|
2282
|
+
return {
|
|
2283
|
+
success: input.success,
|
|
2284
|
+
message: input.summary,
|
|
2285
|
+
filesModified: input.files_modified || filesModified
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
if (filesModified.length > 0) {
|
|
2289
|
+
return {
|
|
2290
|
+
success: false,
|
|
2291
|
+
message: "Agent stopped unexpectedly. Files were modified but installation may be incomplete.",
|
|
2292
|
+
error: "Agent did not call task_complete. Please verify the installation manually.",
|
|
2293
|
+
filesModified
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
return {
|
|
2297
|
+
success: false,
|
|
2298
|
+
error: "Agent stopped without completing the installation task."
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
if (claudeResponse.stop_reason === "tool_use") {
|
|
2302
|
+
const toolUseBlocks = claudeResponse.content.filter(
|
|
2303
|
+
(block) => block.type === "tool_use"
|
|
2304
|
+
);
|
|
2305
|
+
const toolResults = [];
|
|
2306
|
+
for (const toolUse of toolUseBlocks) {
|
|
2307
|
+
if (toolUse.name === "task_complete") {
|
|
2308
|
+
const input = toolUse.input;
|
|
2309
|
+
return {
|
|
2310
|
+
success: input.success,
|
|
2311
|
+
message: input.summary,
|
|
2312
|
+
filesModified: input.files_modified || filesModified
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
const result = await executeTool(toolUse, cwd, debug);
|
|
2316
|
+
if (toolUse.name === "write_file" && !result.is_error) {
|
|
2317
|
+
const path3 = toolUse.input.path;
|
|
2318
|
+
if (!filesModified.includes(path3)) {
|
|
2319
|
+
filesModified.push(path3);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
toolResults.push({
|
|
2323
|
+
type: "tool_result",
|
|
2324
|
+
tool_use_id: toolUse.id,
|
|
2325
|
+
content: result.content,
|
|
2326
|
+
is_error: result.is_error
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
messages.push({ role: "user", content: toolResults });
|
|
2330
|
+
}
|
|
2331
|
+
} catch (error) {
|
|
2332
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2333
|
+
if (debug) {
|
|
2334
|
+
console.error("[DEBUG] Agent error:", error);
|
|
2335
|
+
}
|
|
2336
|
+
return { success: false, error: `Agent error: ${errorMessage}` };
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
return { success: false, error: "Agent exceeded maximum iterations" };
|
|
2340
|
+
}
|
|
2341
|
+
function isPathSafe(basePath, targetPath) {
|
|
2342
|
+
const resolvedBase = (0, import_path3.resolve)(basePath);
|
|
2343
|
+
const resolvedTarget = (0, import_path3.resolve)(basePath, targetPath);
|
|
2344
|
+
return resolvedTarget.startsWith(resolvedBase + "/") || resolvedTarget === resolvedBase;
|
|
2345
|
+
}
|
|
2346
|
+
async function executeTool(toolUse, cwd, debug) {
|
|
2347
|
+
const { name, input } = toolUse;
|
|
2348
|
+
if (debug) {
|
|
2349
|
+
console.log(`[DEBUG] Executing tool: ${name}`, input);
|
|
2350
|
+
}
|
|
2351
|
+
try {
|
|
2352
|
+
switch (name) {
|
|
2353
|
+
case "read_file": {
|
|
2354
|
+
if (typeof input.path !== "string" || !input.path.trim()) {
|
|
2355
|
+
return { content: "Error: path must be a non-empty string", is_error: true };
|
|
2356
|
+
}
|
|
2357
|
+
const path3 = input.path.trim();
|
|
2358
|
+
if (!isPathSafe(cwd, path3)) {
|
|
2359
|
+
return { content: "Error: path traversal detected - access denied", is_error: true };
|
|
2360
|
+
}
|
|
2361
|
+
const fullPath = (0, import_path3.join)(cwd, path3);
|
|
2362
|
+
const content = await (0, import_promises.readFile)(fullPath, "utf-8");
|
|
2363
|
+
return { content };
|
|
2364
|
+
}
|
|
2365
|
+
case "write_file": {
|
|
2366
|
+
if (typeof input.path !== "string" || !input.path.trim()) {
|
|
2367
|
+
return { content: "Error: path must be a non-empty string", is_error: true };
|
|
2368
|
+
}
|
|
2369
|
+
if (typeof input.content !== "string") {
|
|
2370
|
+
return { content: "Error: content must be a string", is_error: true };
|
|
2371
|
+
}
|
|
2372
|
+
const path3 = input.path.trim();
|
|
2373
|
+
const content = input.content;
|
|
2374
|
+
if (!isPathSafe(cwd, path3)) {
|
|
2375
|
+
return { content: "Error: path traversal detected - access denied", is_error: true };
|
|
2376
|
+
}
|
|
2377
|
+
const fullPath = (0, import_path3.join)(cwd, path3);
|
|
2378
|
+
const dir = (0, import_path3.dirname)(fullPath);
|
|
2379
|
+
if (!(0, import_fs2.existsSync)(dir)) {
|
|
2380
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
2381
|
+
}
|
|
2382
|
+
await (0, import_promises.writeFile)(fullPath, content, "utf-8");
|
|
2383
|
+
return { content: `Successfully wrote ${path3}` };
|
|
2384
|
+
}
|
|
2385
|
+
case "run_command": {
|
|
2386
|
+
if (typeof input.command !== "string" || !input.command.trim()) {
|
|
2387
|
+
return { content: "Error: command must be a non-empty string", is_error: true };
|
|
2388
|
+
}
|
|
2389
|
+
const command = input.command.trim();
|
|
2390
|
+
const validation = validateBashCommand(command);
|
|
2391
|
+
if (!validation.allowed) {
|
|
2392
|
+
return {
|
|
2393
|
+
content: `Command blocked: ${validation.reason}`,
|
|
2394
|
+
is_error: true
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
const { stdout, stderr } = await execAsync(command, { cwd, timeout: 12e4 });
|
|
2398
|
+
return { content: stdout || stderr || "Command completed successfully" };
|
|
2399
|
+
}
|
|
2400
|
+
case "list_files": {
|
|
2401
|
+
if (typeof input.path !== "string") {
|
|
2402
|
+
return { content: "Error: path must be a string", is_error: true };
|
|
2403
|
+
}
|
|
2404
|
+
const path3 = input.path.trim() || ".";
|
|
2405
|
+
const pattern = typeof input.pattern === "string" ? input.pattern : "*";
|
|
2406
|
+
if (!isPathSafe(cwd, path3)) {
|
|
2407
|
+
return { content: "Error: path traversal detected - access denied", is_error: true };
|
|
2408
|
+
}
|
|
2409
|
+
const fullPath = (0, import_path3.join)(cwd, path3);
|
|
2410
|
+
const files = await (0, import_glob2.glob)(pattern, {
|
|
2411
|
+
cwd: fullPath,
|
|
2412
|
+
nodir: false,
|
|
2413
|
+
maxDepth: 3
|
|
2414
|
+
});
|
|
2415
|
+
return { content: files.slice(0, 100).join("\n") };
|
|
2416
|
+
}
|
|
2417
|
+
default:
|
|
2418
|
+
return { content: `Unknown tool: ${name}`, is_error: true };
|
|
2419
|
+
}
|
|
2420
|
+
} catch (error) {
|
|
2421
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2422
|
+
return { content: `Error: ${errorMessage}`, is_error: true };
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
function buildIntegrationPrompt(framework, apiKey, docs, enableContainer, workspaceId) {
|
|
2426
|
+
const containerConfig = enableContainer ? " enableContainer: true, // Load third-party pixels from Datalyr dashboard" : " enableContainer: false, // Container scripts disabled";
|
|
2427
|
+
const envVarName = framework === "nextjs" ? "NEXT_PUBLIC_DATALYR_WORKSPACE_ID" : framework === "sveltekit" ? "PUBLIC_DATALYR_WORKSPACE_ID" : framework.startsWith("react") && framework !== "react-native" ? "VITE_DATALYR_WORKSPACE_ID" : "DATALYR_WORKSPACE_ID";
|
|
2428
|
+
return `Install Datalyr analytics into this ${getFrameworkDisplayName(framework)} project.
|
|
2429
|
+
|
|
2430
|
+
## Workspace ID
|
|
2431
|
+
${workspaceId}
|
|
2432
|
+
|
|
2433
|
+
## API Key (for server-side only)
|
|
2434
|
+
${apiKey}
|
|
2435
|
+
|
|
2436
|
+
## Integration Documentation
|
|
2437
|
+
${docs}
|
|
2438
|
+
|
|
2439
|
+
## SDK Configuration
|
|
2440
|
+
When initializing the SDK, use these options:
|
|
2441
|
+
\`\`\`typescript
|
|
2442
|
+
datalyr.init({
|
|
2443
|
+
workspaceId: process.env.${envVarName},
|
|
2444
|
+
${containerConfig}
|
|
2445
|
+
});
|
|
2446
|
+
\`\`\`
|
|
2447
|
+
|
|
2448
|
+
## Environment Variable
|
|
2449
|
+
Add to .env.local (or .env):
|
|
2450
|
+
${envVarName}=${workspaceId}
|
|
2451
|
+
|
|
2452
|
+
## Your Task
|
|
2453
|
+
|
|
2454
|
+
1. Read package.json to understand the project
|
|
2455
|
+
2. Install the SDK: run the appropriate install command
|
|
2456
|
+
3. Create initialization code following the documentation
|
|
2457
|
+
4. Add the environment variable to .env.local or .env
|
|
2458
|
+
5. Call task_complete when done
|
|
2459
|
+
|
|
2460
|
+
Start by reading package.json.`;
|
|
2461
|
+
}
|
|
2462
|
+
async function validateApiKey(apiKey) {
|
|
2463
|
+
const controller = new AbortController();
|
|
2464
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
2465
|
+
const response = await fetch(`${LLM_GATEWAY_URL}/validate-key`, {
|
|
2466
|
+
method: "POST",
|
|
2467
|
+
headers: {
|
|
2468
|
+
"Content-Type": "application/json"
|
|
2469
|
+
},
|
|
2470
|
+
body: JSON.stringify({ apiKey }),
|
|
2471
|
+
signal: controller.signal
|
|
2472
|
+
});
|
|
2473
|
+
clearTimeout(timeoutId);
|
|
2474
|
+
if (!response.ok) {
|
|
2475
|
+
const error = await response.json();
|
|
2476
|
+
throw new Error(error.error || "Failed to validate API key");
|
|
2477
|
+
}
|
|
2478
|
+
const result = await response.json();
|
|
2479
|
+
if (!result.success || !result.workspace) {
|
|
2480
|
+
throw new Error(result.error || "Invalid API key");
|
|
2481
|
+
}
|
|
2482
|
+
return result.workspace;
|
|
2483
|
+
}
|
|
2484
|
+
async function fetchWorkspaces(apiKey) {
|
|
2485
|
+
const controller = new AbortController();
|
|
2486
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
2487
|
+
const response = await fetch(`${LLM_GATEWAY_URL}/workspaces`, {
|
|
2488
|
+
method: "POST",
|
|
2489
|
+
headers: {
|
|
2490
|
+
"Content-Type": "application/json"
|
|
2491
|
+
},
|
|
2492
|
+
body: JSON.stringify({ apiKey }),
|
|
2493
|
+
signal: controller.signal
|
|
2494
|
+
});
|
|
2495
|
+
clearTimeout(timeoutId);
|
|
2496
|
+
if (!response.ok) {
|
|
2497
|
+
return [];
|
|
2498
|
+
}
|
|
2499
|
+
const result = await response.json();
|
|
2500
|
+
if (!result.success || !result.workspaces) {
|
|
2501
|
+
return [];
|
|
2502
|
+
}
|
|
2503
|
+
return result.workspaces;
|
|
2504
|
+
}
|
|
2505
|
+
async function verifyInstallation(apiKey, workspaceId) {
|
|
2506
|
+
try {
|
|
2507
|
+
const controller = new AbortController();
|
|
2508
|
+
const timeoutId = setTimeout(() => controller.abort(), 15e3);
|
|
2509
|
+
const response = await fetch(`${LLM_GATEWAY_URL}/verify`, {
|
|
2510
|
+
method: "POST",
|
|
2511
|
+
headers: {
|
|
2512
|
+
"Content-Type": "application/json"
|
|
2513
|
+
},
|
|
2514
|
+
body: JSON.stringify({ apiKey, workspaceId }),
|
|
2515
|
+
signal: controller.signal
|
|
2516
|
+
});
|
|
2517
|
+
clearTimeout(timeoutId);
|
|
2518
|
+
if (!response.ok) {
|
|
2519
|
+
return { success: false };
|
|
2520
|
+
}
|
|
2521
|
+
const result = await response.json();
|
|
2522
|
+
return result;
|
|
2523
|
+
} catch {
|
|
2524
|
+
return { success: false };
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// bin/wizard.ts
|
|
2529
|
+
var validFrameworks = [
|
|
2530
|
+
"nextjs",
|
|
2531
|
+
"react",
|
|
2532
|
+
"react-vite",
|
|
2533
|
+
"svelte",
|
|
2534
|
+
"sveltekit",
|
|
2535
|
+
"react-native",
|
|
2536
|
+
"expo",
|
|
2537
|
+
"ios",
|
|
2538
|
+
"node"
|
|
2539
|
+
];
|
|
2540
|
+
async function main() {
|
|
2541
|
+
const argv = await (0, import_yargs.default)((0, import_helpers.hideBin)(process.argv)).scriptName("datalyr-wizard").usage("$0 [options]").option("api-key", {
|
|
2542
|
+
alias: "k",
|
|
2543
|
+
type: "string",
|
|
2544
|
+
description: "Your Datalyr API key (starts with dk_)"
|
|
2545
|
+
}).option("framework", {
|
|
2546
|
+
alias: "f",
|
|
2547
|
+
type: "string",
|
|
2548
|
+
choices: validFrameworks,
|
|
2549
|
+
description: "Framework to configure"
|
|
2550
|
+
}).option("skip-verification", {
|
|
2551
|
+
type: "boolean",
|
|
2552
|
+
default: false,
|
|
2553
|
+
description: "Skip post-install verification"
|
|
2554
|
+
}).option("enable-container", {
|
|
2555
|
+
type: "boolean",
|
|
2556
|
+
description: "Enable/disable container scripts for pixel management"
|
|
2557
|
+
}).option("debug", {
|
|
2558
|
+
type: "boolean",
|
|
2559
|
+
default: false,
|
|
2560
|
+
description: "Enable debug output"
|
|
2561
|
+
}).option("cwd", {
|
|
2562
|
+
type: "string",
|
|
2563
|
+
description: "Working directory (defaults to current)"
|
|
2564
|
+
}).example("$0", "Run interactive wizard").example("$0 --api-key dk_xxx", "Pre-fill API key").example("$0 --framework nextjs", "Force Next.js configuration").help().alias("help", "h").version().alias("version", "v").parse();
|
|
2565
|
+
const options = {
|
|
2566
|
+
apiKey: argv["api-key"],
|
|
2567
|
+
framework: argv.framework,
|
|
2568
|
+
skipVerification: argv["skip-verification"],
|
|
2569
|
+
enableContainer: argv["enable-container"],
|
|
2570
|
+
debug: argv.debug,
|
|
2571
|
+
cwd: argv.cwd
|
|
2572
|
+
};
|
|
2573
|
+
const result = await runAgentWizard(NEXTJS_CONFIG, options);
|
|
2574
|
+
process.exit(result.success ? 0 : 1);
|
|
2575
|
+
}
|
|
2576
|
+
main().catch((error) => {
|
|
2577
|
+
console.error("Fatal error:", error);
|
|
2578
|
+
process.exit(1);
|
|
2579
|
+
});
|
|
2580
|
+
//# sourceMappingURL=wizard.js.map
|