@datalyr/wizard 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/wizard.js +15 -2
- package/dist/bin/wizard.js.map +1 -1
- package/dist/index.d.mts +60 -1
- package/dist/index.d.ts +60 -1
- package/dist/index.js +2392 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2389 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -2
package/dist/index.js
CHANGED
|
@@ -32,10 +32,11 @@ var src_exports = {};
|
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
detectFramework: () => detectFramework,
|
|
34
34
|
generateInstallationPlan: () => generateInstallationPlan,
|
|
35
|
+
runAgentWizard: () => runAgentWizard,
|
|
35
36
|
runWizard: () => runWizard
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(src_exports);
|
|
38
|
-
var
|
|
39
|
+
var import_path6 = __toESM(require("path"));
|
|
39
40
|
|
|
40
41
|
// src/detection/detector.ts
|
|
41
42
|
var import_path2 = __toESM(require("path"));
|
|
@@ -264,6 +265,8 @@ function getInstallCommand(pm, packages, dev = false) {
|
|
|
264
265
|
return dev ? `pnpm add -D ${pkgList}` : `pnpm add ${pkgList}`;
|
|
265
266
|
case "bun":
|
|
266
267
|
return dev ? `bun add -d ${pkgList}` : `bun add ${pkgList}`;
|
|
268
|
+
default:
|
|
269
|
+
return dev ? `npm install -D ${pkgList}` : `npm install ${pkgList}`;
|
|
267
270
|
}
|
|
268
271
|
}
|
|
269
272
|
|
|
@@ -489,29 +492,39 @@ export { datalyr };
|
|
|
489
492
|
|
|
490
493
|
// src/generators/templates/react-native.ts
|
|
491
494
|
function generateReactNativeFiles(options) {
|
|
492
|
-
const { language, isExpo
|
|
495
|
+
const { language, isExpo } = options;
|
|
493
496
|
const ext = language === "typescript" ? "ts" : "js";
|
|
494
497
|
return [
|
|
495
498
|
{
|
|
496
499
|
path: `src/utils/datalyr.${ext}`,
|
|
497
500
|
action: "create",
|
|
498
501
|
description: "Datalyr initialization",
|
|
499
|
-
content: generateReactNativeInit(language, isExpo
|
|
502
|
+
content: generateReactNativeInit(language, isExpo)
|
|
500
503
|
}
|
|
501
504
|
];
|
|
502
505
|
}
|
|
503
|
-
function generateReactNativeInit(language, isExpo
|
|
506
|
+
function generateReactNativeInit(language, isExpo) {
|
|
504
507
|
const importPath = isExpo ? "@datalyr/react-native/expo" : "@datalyr/react-native";
|
|
505
508
|
if (language === "typescript") {
|
|
506
509
|
return `import { Datalyr } from '${importPath}';
|
|
507
510
|
|
|
511
|
+
// IMPORTANT: Set DATALYR_API_KEY in your environment
|
|
512
|
+
// For Expo: Add to app.config.js extra field and use expo-constants
|
|
513
|
+
// For bare RN: Use react-native-config or babel-plugin-transform-inline-environment-variables
|
|
514
|
+
const DATALYR_API_KEY = process.env.DATALYR_API_KEY;
|
|
515
|
+
|
|
508
516
|
let initialized = false;
|
|
509
517
|
|
|
510
518
|
export async function initDatalyr(): Promise<void> {
|
|
511
519
|
if (initialized) return;
|
|
512
520
|
|
|
521
|
+
if (!DATALYR_API_KEY) {
|
|
522
|
+
console.warn('Datalyr: API key not configured. Set DATALYR_API_KEY environment variable.');
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
513
526
|
await Datalyr.initialize({
|
|
514
|
-
apiKey:
|
|
527
|
+
apiKey: DATALYR_API_KEY,
|
|
515
528
|
enableAutoEvents: true,
|
|
516
529
|
enableAttribution: true,
|
|
517
530
|
debug: __DEV__,
|
|
@@ -536,13 +549,23 @@ export function identifyUser(userId: string, traits?: Record<string, unknown>):
|
|
|
536
549
|
}
|
|
537
550
|
return `import { Datalyr } from '${importPath}';
|
|
538
551
|
|
|
552
|
+
// IMPORTANT: Set DATALYR_API_KEY in your environment
|
|
553
|
+
// For Expo: Add to app.config.js extra field and use expo-constants
|
|
554
|
+
// For bare RN: Use react-native-config or babel-plugin-transform-inline-environment-variables
|
|
555
|
+
const DATALYR_API_KEY = process.env.DATALYR_API_KEY;
|
|
556
|
+
|
|
539
557
|
let initialized = false;
|
|
540
558
|
|
|
541
559
|
export async function initDatalyr() {
|
|
542
560
|
if (initialized) return;
|
|
543
561
|
|
|
562
|
+
if (!DATALYR_API_KEY) {
|
|
563
|
+
console.warn('Datalyr: API key not configured. Set DATALYR_API_KEY environment variable.');
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
544
567
|
await Datalyr.initialize({
|
|
545
|
-
apiKey:
|
|
568
|
+
apiKey: DATALYR_API_KEY,
|
|
546
569
|
enableAutoEvents: true,
|
|
547
570
|
enableAttribution: true,
|
|
548
571
|
debug: __DEV__,
|
|
@@ -573,22 +596,22 @@ var import_path4 = __toESM(require("path"));
|
|
|
573
596
|
var import_ora = __toESM(require("ora"));
|
|
574
597
|
var import_chalk = __toESM(require("chalk"));
|
|
575
598
|
var currentSpinner = null;
|
|
576
|
-
function startSpinner(
|
|
599
|
+
function startSpinner(text2) {
|
|
577
600
|
if (currentSpinner) {
|
|
578
601
|
currentSpinner.stop();
|
|
579
602
|
}
|
|
580
|
-
currentSpinner = (0, import_ora.default)({ text, color: "cyan" }).start();
|
|
603
|
+
currentSpinner = (0, import_ora.default)({ text: text2, color: "cyan" }).start();
|
|
581
604
|
return currentSpinner;
|
|
582
605
|
}
|
|
583
|
-
function succeedSpinner(
|
|
606
|
+
function succeedSpinner(text2) {
|
|
584
607
|
if (currentSpinner) {
|
|
585
|
-
currentSpinner.succeed(
|
|
608
|
+
currentSpinner.succeed(text2);
|
|
586
609
|
currentSpinner = null;
|
|
587
610
|
}
|
|
588
611
|
}
|
|
589
|
-
function failSpinner(
|
|
612
|
+
function failSpinner(text2) {
|
|
590
613
|
if (currentSpinner) {
|
|
591
|
-
currentSpinner.fail(
|
|
614
|
+
currentSpinner.fail(text2);
|
|
592
615
|
currentSpinner = null;
|
|
593
616
|
}
|
|
594
617
|
}
|
|
@@ -688,7 +711,7 @@ async function updateEnvFile(cwd, envVars, options = {}) {
|
|
|
688
711
|
if (options.dryRun) {
|
|
689
712
|
return true;
|
|
690
713
|
}
|
|
691
|
-
const
|
|
714
|
+
const spinner2 = startSpinner(`Updating ${envFileName}...`);
|
|
692
715
|
try {
|
|
693
716
|
let existingContent = "";
|
|
694
717
|
if (await fileExists(envPath)) {
|
|
@@ -751,9 +774,27 @@ function parseEnvFile(content) {
|
|
|
751
774
|
}
|
|
752
775
|
function getEnvVarsForFramework(framework, apiKey) {
|
|
753
776
|
const vars = [];
|
|
777
|
+
const getPublicEnvVarKey = (fw) => {
|
|
778
|
+
switch (fw) {
|
|
779
|
+
case "react":
|
|
780
|
+
case "react-vite":
|
|
781
|
+
return "VITE_DATALYR_WORKSPACE_ID";
|
|
782
|
+
case "svelte":
|
|
783
|
+
case "sveltekit":
|
|
784
|
+
return "PUBLIC_DATALYR_WORKSPACE_ID";
|
|
785
|
+
case "vue":
|
|
786
|
+
case "nuxt":
|
|
787
|
+
return "NUXT_PUBLIC_DATALYR_WORKSPACE_ID";
|
|
788
|
+
case "remix":
|
|
789
|
+
case "astro":
|
|
790
|
+
case "nextjs":
|
|
791
|
+
default:
|
|
792
|
+
return "NEXT_PUBLIC_DATALYR_WORKSPACE_ID";
|
|
793
|
+
}
|
|
794
|
+
};
|
|
754
795
|
if (["nextjs", "react", "react-vite", "svelte", "sveltekit", "vue", "nuxt", "remix", "astro"].includes(framework)) {
|
|
755
796
|
vars.push({
|
|
756
|
-
key:
|
|
797
|
+
key: getPublicEnvVarKey(framework),
|
|
757
798
|
value: "",
|
|
758
799
|
description: "Your Datalyr workspace ID (get from dashboard)",
|
|
759
800
|
isPublic: true
|
|
@@ -789,7 +830,7 @@ function generateFilesForFramework(context, apiKey) {
|
|
|
789
830
|
case "nextjs":
|
|
790
831
|
return generateNextJSFiles({
|
|
791
832
|
language,
|
|
792
|
-
hasAppRouter: context.entryPoints.some((
|
|
833
|
+
hasAppRouter: context.entryPoints.some((p2) => p2.includes("app/layout")),
|
|
793
834
|
apiKey
|
|
794
835
|
});
|
|
795
836
|
case "react":
|
|
@@ -802,8 +843,7 @@ function generateFilesForFramework(context, apiKey) {
|
|
|
802
843
|
case "expo":
|
|
803
844
|
return generateReactNativeFiles({
|
|
804
845
|
language,
|
|
805
|
-
isExpo: framework === "expo"
|
|
806
|
-
apiKey
|
|
846
|
+
isExpo: framework === "expo"
|
|
807
847
|
});
|
|
808
848
|
case "sveltekit":
|
|
809
849
|
return generateSvelteKitFiles(language, apiKey);
|
|
@@ -1053,7 +1093,7 @@ async function installPackages(cwd, packages, options = {}) {
|
|
|
1053
1093
|
if (options.dryRun) return true;
|
|
1054
1094
|
const pm = await detectPackageManager(cwd);
|
|
1055
1095
|
const command = getInstallCommand(pm, packages, options.dev);
|
|
1056
|
-
const
|
|
1096
|
+
const spinner2 = startSpinner(`Installing ${packages.join(", ")}...`);
|
|
1057
1097
|
try {
|
|
1058
1098
|
const [cmd, ...args] = command.split(" ");
|
|
1059
1099
|
await (0, import_execa.execa)(cmd, args, { cwd, stdio: "pipe" });
|
|
@@ -1149,6 +1189,2336 @@ var logger = {
|
|
|
1149
1189
|
blank: () => console.log()
|
|
1150
1190
|
};
|
|
1151
1191
|
|
|
1192
|
+
// src/agent/runner.ts
|
|
1193
|
+
var p = __toESM(require("@clack/prompts"));
|
|
1194
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
1195
|
+
|
|
1196
|
+
// src/agent/interface.ts
|
|
1197
|
+
var AGENT_SIGNALS = {
|
|
1198
|
+
STATUS: "[STATUS]",
|
|
1199
|
+
ERROR_MISSING_KEY: "[ERROR_MISSING_KEY]",
|
|
1200
|
+
ERROR_FAILED: "[ERROR_FAILED]",
|
|
1201
|
+
SUCCESS: "[SUCCESS]"
|
|
1202
|
+
};
|
|
1203
|
+
var ALLOWED_COMMANDS = [
|
|
1204
|
+
// Package managers
|
|
1205
|
+
"npm",
|
|
1206
|
+
"yarn",
|
|
1207
|
+
"pnpm",
|
|
1208
|
+
"bun",
|
|
1209
|
+
"npx",
|
|
1210
|
+
// Build tools
|
|
1211
|
+
"tsc",
|
|
1212
|
+
"node",
|
|
1213
|
+
// iOS
|
|
1214
|
+
"pod",
|
|
1215
|
+
"xcodebuild",
|
|
1216
|
+
// File operations (read-only)
|
|
1217
|
+
"cat",
|
|
1218
|
+
"ls",
|
|
1219
|
+
"find",
|
|
1220
|
+
"grep",
|
|
1221
|
+
"head",
|
|
1222
|
+
"tail",
|
|
1223
|
+
"wc",
|
|
1224
|
+
// Git (read-only)
|
|
1225
|
+
"git status",
|
|
1226
|
+
"git log",
|
|
1227
|
+
"git diff",
|
|
1228
|
+
"git branch"
|
|
1229
|
+
];
|
|
1230
|
+
var BLOCKED_PATTERNS = [
|
|
1231
|
+
/;/,
|
|
1232
|
+
// Command chaining
|
|
1233
|
+
/`/,
|
|
1234
|
+
// Backticks
|
|
1235
|
+
/\$\(/,
|
|
1236
|
+
// Command substitution
|
|
1237
|
+
/\$\w/,
|
|
1238
|
+
// Variable expansion (could contain malicious code)
|
|
1239
|
+
/\|\s*sh/,
|
|
1240
|
+
// Piping to shell
|
|
1241
|
+
/\|\s*bash/,
|
|
1242
|
+
// Piping to bash
|
|
1243
|
+
/\|\s*zsh/,
|
|
1244
|
+
// Piping to zsh
|
|
1245
|
+
/\|\|/,
|
|
1246
|
+
// Or operator (allows fallback commands)
|
|
1247
|
+
/\s&\s/,
|
|
1248
|
+
// Background execution mid-command
|
|
1249
|
+
/\s&$/,
|
|
1250
|
+
// Background execution at end
|
|
1251
|
+
/rm\s+-rf/,
|
|
1252
|
+
// Dangerous rm
|
|
1253
|
+
/rm\s+-r/,
|
|
1254
|
+
// Recursive rm
|
|
1255
|
+
/>\s*\//,
|
|
1256
|
+
// Overwriting system files
|
|
1257
|
+
/&&\s*rm/,
|
|
1258
|
+
// rm after &&
|
|
1259
|
+
/\|\s*rm/
|
|
1260
|
+
// rm after pipe
|
|
1261
|
+
];
|
|
1262
|
+
function validateBashCommand(command) {
|
|
1263
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
1264
|
+
if (pattern.test(command)) {
|
|
1265
|
+
return { allowed: false, reason: `Blocked pattern detected: ${pattern}` };
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const baseCommand = command.trim().split(/\s+/)[0];
|
|
1269
|
+
const isAllowed = ALLOWED_COMMANDS.some((allowed) => {
|
|
1270
|
+
if (allowed.includes(" ")) {
|
|
1271
|
+
return command.startsWith(allowed);
|
|
1272
|
+
}
|
|
1273
|
+
return baseCommand === allowed;
|
|
1274
|
+
});
|
|
1275
|
+
if (!isAllowed) {
|
|
1276
|
+
return { allowed: false, reason: `Command not in allowlist: ${baseCommand}` };
|
|
1277
|
+
}
|
|
1278
|
+
return { allowed: true };
|
|
1279
|
+
}
|
|
1280
|
+
function buildSystemPrompt() {
|
|
1281
|
+
return `<role>
|
|
1282
|
+
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.
|
|
1283
|
+
</role>
|
|
1284
|
+
|
|
1285
|
+
<task>
|
|
1286
|
+
Install and configure the Datalyr analytics SDK in the user's project. Complete this task by:
|
|
1287
|
+
1. Detecting the framework and understanding the project structure
|
|
1288
|
+
2. Installing the correct SDK packages
|
|
1289
|
+
3. Creating initialization code in the appropriate entry point
|
|
1290
|
+
4. Configuring environment variables
|
|
1291
|
+
</task>
|
|
1292
|
+
|
|
1293
|
+
<sdks>
|
|
1294
|
+
| SDK | Use Case | Install Command |
|
|
1295
|
+
|-----|----------|-----------------|
|
|
1296
|
+
| @datalyr/web | Browser apps (React, Vue, Svelte, Next.js client) | npm install @datalyr/web |
|
|
1297
|
+
| @datalyr/api | Server-side (Next.js API routes, Express, Node) | npm install @datalyr/api |
|
|
1298
|
+
| @datalyr/react-native | React Native & Expo mobile apps | npm install @datalyr/react-native |
|
|
1299
|
+
| DatalyrSDK | Native iOS Swift apps | Swift Package Manager |
|
|
1300
|
+
</sdks>
|
|
1301
|
+
|
|
1302
|
+
<rules>
|
|
1303
|
+
1. ALWAYS read files before modifying them - never edit blind
|
|
1304
|
+
2. PRESERVE existing code - only add Datalyr, never remove functionality
|
|
1305
|
+
3. MATCH the project's code style (indentation, quotes, semicolons)
|
|
1306
|
+
4. USE TypeScript if the project uses TypeScript
|
|
1307
|
+
5. PLACE initialization in the correct entry point for the framework
|
|
1308
|
+
6. UPDATE .env or .env.local with NEXT_PUBLIC_DATALYR_WORKSPACE_ID
|
|
1309
|
+
7. CHECK for existing Datalyr setup first - don't duplicate
|
|
1310
|
+
</rules>
|
|
1311
|
+
|
|
1312
|
+
<workflow>
|
|
1313
|
+
Step 1: Read package.json to detect framework and package manager
|
|
1314
|
+
Step 2: List files to find entry points (app/layout.tsx, src/main.tsx, App.tsx, etc.)
|
|
1315
|
+
Step 3: Read entry point files to understand current structure
|
|
1316
|
+
Step 4: Install SDK packages using detected package manager
|
|
1317
|
+
Step 5: Create initialization file (lib/datalyr.ts or similar)
|
|
1318
|
+
Step 6: Update entry point to import and initialize Datalyr
|
|
1319
|
+
Step 7: Update or create .env.local with workspace ID placeholder
|
|
1320
|
+
Step 8: Call task_complete with summary of changes
|
|
1321
|
+
</workflow>
|
|
1322
|
+
|
|
1323
|
+
<examples>
|
|
1324
|
+
<example name="nextjs-app-router">
|
|
1325
|
+
For Next.js 13+ with App Router, create app/providers.tsx:
|
|
1326
|
+
\`\`\`tsx
|
|
1327
|
+
'use client';
|
|
1328
|
+
import datalyr from '@datalyr/web';
|
|
1329
|
+
import { useEffect } from 'react';
|
|
1330
|
+
|
|
1331
|
+
export function DatalyrProvider({ children }: { children: React.ReactNode }) {
|
|
1332
|
+
useEffect(() => {
|
|
1333
|
+
datalyr.init({
|
|
1334
|
+
workspaceId: process.env.NEXT_PUBLIC_DATALYR_WORKSPACE_ID!,
|
|
1335
|
+
debug: process.env.NODE_ENV === 'development',
|
|
1336
|
+
});
|
|
1337
|
+
}, []);
|
|
1338
|
+
return <>{children}</>;
|
|
1339
|
+
}
|
|
1340
|
+
\`\`\`
|
|
1341
|
+
Then wrap children in app/layout.tsx with <DatalyrProvider>.
|
|
1342
|
+
</example>
|
|
1343
|
+
|
|
1344
|
+
<example name="react-vite">
|
|
1345
|
+
For React + Vite, create src/lib/datalyr.ts:
|
|
1346
|
+
\`\`\`ts
|
|
1347
|
+
import datalyr from '@datalyr/web';
|
|
1348
|
+
|
|
1349
|
+
let initialized = false;
|
|
1350
|
+
|
|
1351
|
+
export function initDatalyr() {
|
|
1352
|
+
if (initialized) return;
|
|
1353
|
+
datalyr.init({
|
|
1354
|
+
workspaceId: import.meta.env.VITE_DATALYR_WORKSPACE_ID,
|
|
1355
|
+
debug: import.meta.env.DEV,
|
|
1356
|
+
});
|
|
1357
|
+
initialized = true;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
export { datalyr };
|
|
1361
|
+
\`\`\`
|
|
1362
|
+
Then call initDatalyr() at the top of src/main.tsx.
|
|
1363
|
+
</example>
|
|
1364
|
+
|
|
1365
|
+
<example name="react-native">
|
|
1366
|
+
For React Native, create src/utils/datalyr.ts:
|
|
1367
|
+
\`\`\`ts
|
|
1368
|
+
import { Datalyr } from '@datalyr/react-native';
|
|
1369
|
+
|
|
1370
|
+
export async function initDatalyr(apiKey: string) {
|
|
1371
|
+
await Datalyr.initialize({
|
|
1372
|
+
apiKey,
|
|
1373
|
+
enableAutoEvents: true,
|
|
1374
|
+
debug: __DEV__,
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
export { Datalyr };
|
|
1379
|
+
\`\`\`
|
|
1380
|
+
Then call initDatalyr() in App.tsx useEffect.
|
|
1381
|
+
</example>
|
|
1382
|
+
</examples>
|
|
1383
|
+
|
|
1384
|
+
<signals>
|
|
1385
|
+
When complete: ${AGENT_SIGNALS.SUCCESS}
|
|
1386
|
+
On error: ${AGENT_SIGNALS.ERROR_FAILED}
|
|
1387
|
+
Status updates: ${AGENT_SIGNALS.STATUS} <message>
|
|
1388
|
+
</signals>`;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// src/agent/docs/index.ts
|
|
1392
|
+
function getFrameworkDocs(framework, apiKey) {
|
|
1393
|
+
switch (framework) {
|
|
1394
|
+
case "nextjs":
|
|
1395
|
+
return getNextjsDocs(apiKey);
|
|
1396
|
+
case "react":
|
|
1397
|
+
case "react-vite":
|
|
1398
|
+
return getReactDocs(apiKey);
|
|
1399
|
+
case "sveltekit":
|
|
1400
|
+
return getSvelteKitDocs(apiKey);
|
|
1401
|
+
case "svelte":
|
|
1402
|
+
return getSvelteDocs(apiKey);
|
|
1403
|
+
case "react-native":
|
|
1404
|
+
case "expo":
|
|
1405
|
+
return getReactNativeDocs(apiKey, framework === "expo");
|
|
1406
|
+
case "ios":
|
|
1407
|
+
return getIOSDocs(apiKey);
|
|
1408
|
+
case "node":
|
|
1409
|
+
return getNodeDocs(apiKey);
|
|
1410
|
+
default:
|
|
1411
|
+
return getGenericWebDocs(apiKey);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function getNextjsDocs(apiKey) {
|
|
1415
|
+
return `
|
|
1416
|
+
# Datalyr Next.js Integration
|
|
1417
|
+
|
|
1418
|
+
## Packages to Install
|
|
1419
|
+
- @datalyr/web (client-side tracking)
|
|
1420
|
+
- @datalyr/api (server-side tracking)
|
|
1421
|
+
|
|
1422
|
+
## Environment Variables
|
|
1423
|
+
Add to .env.local:
|
|
1424
|
+
\`\`\`
|
|
1425
|
+
NEXT_PUBLIC_DATALYR_WORKSPACE_ID=your_workspace_id
|
|
1426
|
+
DATALYR_API_KEY=${apiKey}
|
|
1427
|
+
\`\`\`
|
|
1428
|
+
|
|
1429
|
+
## App Router Setup (Next.js 13+)
|
|
1430
|
+
|
|
1431
|
+
### 1. Create Provider Component
|
|
1432
|
+
Create \`app/providers.tsx\`:
|
|
1433
|
+
\`\`\`tsx
|
|
1434
|
+
'use client';
|
|
1435
|
+
|
|
1436
|
+
import datalyr from '@datalyr/web';
|
|
1437
|
+
import { useEffect } from 'react';
|
|
1438
|
+
|
|
1439
|
+
export function DatalyrProvider({ children }: { children: React.ReactNode }) {
|
|
1440
|
+
useEffect(() => {
|
|
1441
|
+
datalyr.init({
|
|
1442
|
+
workspaceId: process.env.NEXT_PUBLIC_DATALYR_WORKSPACE_ID!,
|
|
1443
|
+
debug: process.env.NODE_ENV === 'development',
|
|
1444
|
+
trackSPA: true,
|
|
1445
|
+
});
|
|
1446
|
+
}, []);
|
|
1447
|
+
|
|
1448
|
+
return <>{children}</>;
|
|
1449
|
+
}
|
|
1450
|
+
\`\`\`
|
|
1451
|
+
|
|
1452
|
+
### 2. Wrap Layout
|
|
1453
|
+
Update \`app/layout.tsx\` to wrap children with the provider:
|
|
1454
|
+
\`\`\`tsx
|
|
1455
|
+
import { DatalyrProvider } from './providers';
|
|
1456
|
+
|
|
1457
|
+
export default function RootLayout({ children }) {
|
|
1458
|
+
return (
|
|
1459
|
+
<html>
|
|
1460
|
+
<body>
|
|
1461
|
+
<DatalyrProvider>
|
|
1462
|
+
{children}
|
|
1463
|
+
</DatalyrProvider>
|
|
1464
|
+
</body>
|
|
1465
|
+
</html>
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
\`\`\`
|
|
1469
|
+
|
|
1470
|
+
### 3. Server-Side Instance (Optional)
|
|
1471
|
+
Create \`lib/datalyr.ts\` for server-side tracking:
|
|
1472
|
+
\`\`\`ts
|
|
1473
|
+
import Datalyr from '@datalyr/api';
|
|
1474
|
+
|
|
1475
|
+
export const datalyr = new Datalyr(process.env.DATALYR_API_KEY!);
|
|
1476
|
+
\`\`\`
|
|
1477
|
+
|
|
1478
|
+
## Pages Router Setup (Legacy)
|
|
1479
|
+
|
|
1480
|
+
### 1. Create Hook
|
|
1481
|
+
Create \`lib/datalyr.ts\`:
|
|
1482
|
+
\`\`\`ts
|
|
1483
|
+
import datalyr from '@datalyr/web';
|
|
1484
|
+
|
|
1485
|
+
let initialized = false;
|
|
1486
|
+
|
|
1487
|
+
export function initDatalyr() {
|
|
1488
|
+
if (initialized || typeof window === 'undefined') return;
|
|
1489
|
+
|
|
1490
|
+
datalyr.init({
|
|
1491
|
+
workspaceId: process.env.NEXT_PUBLIC_DATALYR_WORKSPACE_ID!,
|
|
1492
|
+
debug: process.env.NODE_ENV === 'development',
|
|
1493
|
+
trackSPA: true,
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
initialized = true;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
export { datalyr };
|
|
1500
|
+
\`\`\`
|
|
1501
|
+
|
|
1502
|
+
### 2. Initialize in _app.tsx
|
|
1503
|
+
\`\`\`tsx
|
|
1504
|
+
import { useEffect } from 'react';
|
|
1505
|
+
import { initDatalyr } from '../lib/datalyr';
|
|
1506
|
+
|
|
1507
|
+
function MyApp({ Component, pageProps }) {
|
|
1508
|
+
useEffect(() => {
|
|
1509
|
+
initDatalyr();
|
|
1510
|
+
}, []);
|
|
1511
|
+
|
|
1512
|
+
return <Component {...pageProps} />;
|
|
1513
|
+
}
|
|
1514
|
+
\`\`\`
|
|
1515
|
+
`;
|
|
1516
|
+
}
|
|
1517
|
+
function getReactDocs(apiKey) {
|
|
1518
|
+
return `
|
|
1519
|
+
# Datalyr React Integration
|
|
1520
|
+
|
|
1521
|
+
## Package to Install
|
|
1522
|
+
- @datalyr/web
|
|
1523
|
+
|
|
1524
|
+
## Environment Variables
|
|
1525
|
+
Add to .env (or .env.local for Vite):
|
|
1526
|
+
\`\`\`
|
|
1527
|
+
VITE_DATALYR_WORKSPACE_ID=your_workspace_id
|
|
1528
|
+
# Or for Create React App:
|
|
1529
|
+
REACT_APP_DATALYR_WORKSPACE_ID=your_workspace_id
|
|
1530
|
+
\`\`\`
|
|
1531
|
+
|
|
1532
|
+
## Setup
|
|
1533
|
+
|
|
1534
|
+
### 1. Create Initialization Module
|
|
1535
|
+
Create \`src/lib/datalyr.ts\`:
|
|
1536
|
+
\`\`\`ts
|
|
1537
|
+
import datalyr from '@datalyr/web';
|
|
1538
|
+
|
|
1539
|
+
let initialized = false;
|
|
1540
|
+
|
|
1541
|
+
export function initDatalyr() {
|
|
1542
|
+
if (initialized || typeof window === 'undefined') return;
|
|
1543
|
+
|
|
1544
|
+
datalyr.init({
|
|
1545
|
+
// Vite uses import.meta.env, CRA uses process.env
|
|
1546
|
+
workspaceId: import.meta.env?.VITE_DATALYR_WORKSPACE_ID ||
|
|
1547
|
+
process.env.REACT_APP_DATALYR_WORKSPACE_ID,
|
|
1548
|
+
debug: import.meta.env?.DEV || process.env.NODE_ENV === 'development',
|
|
1549
|
+
trackSPA: true,
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
initialized = true;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
export { datalyr };
|
|
1556
|
+
\`\`\`
|
|
1557
|
+
|
|
1558
|
+
### 2. Initialize in Entry Point
|
|
1559
|
+
Update \`src/main.tsx\` (Vite) or \`src/index.tsx\` (CRA):
|
|
1560
|
+
\`\`\`tsx
|
|
1561
|
+
import { initDatalyr } from './lib/datalyr';
|
|
1562
|
+
|
|
1563
|
+
// Initialize before rendering
|
|
1564
|
+
initDatalyr();
|
|
1565
|
+
|
|
1566
|
+
// ... rest of your app setup
|
|
1567
|
+
\`\`\`
|
|
1568
|
+
|
|
1569
|
+
## Tracking Events
|
|
1570
|
+
\`\`\`ts
|
|
1571
|
+
import { datalyr } from './lib/datalyr';
|
|
1572
|
+
|
|
1573
|
+
// Track an event
|
|
1574
|
+
datalyr.track('button_clicked', { button_id: 'signup' });
|
|
1575
|
+
|
|
1576
|
+
// Identify a user
|
|
1577
|
+
datalyr.identify('user_123', { email: 'user@example.com' });
|
|
1578
|
+
\`\`\`
|
|
1579
|
+
`;
|
|
1580
|
+
}
|
|
1581
|
+
function getSvelteKitDocs(apiKey) {
|
|
1582
|
+
return `
|
|
1583
|
+
# Datalyr SvelteKit Integration
|
|
1584
|
+
|
|
1585
|
+
## Packages to Install
|
|
1586
|
+
- @datalyr/web (client-side)
|
|
1587
|
+
- @datalyr/api (server-side)
|
|
1588
|
+
|
|
1589
|
+
## Environment Variables
|
|
1590
|
+
Add to .env:
|
|
1591
|
+
\`\`\`
|
|
1592
|
+
PUBLIC_DATALYR_WORKSPACE_ID=your_workspace_id
|
|
1593
|
+
DATALYR_API_KEY=${apiKey}
|
|
1594
|
+
\`\`\`
|
|
1595
|
+
|
|
1596
|
+
## Setup
|
|
1597
|
+
|
|
1598
|
+
### 1. Client-Side Initialization
|
|
1599
|
+
Create \`src/lib/datalyr.ts\`:
|
|
1600
|
+
\`\`\`ts
|
|
1601
|
+
import datalyr from '@datalyr/web';
|
|
1602
|
+
import { browser } from '$app/environment';
|
|
1603
|
+
|
|
1604
|
+
let initialized = false;
|
|
1605
|
+
|
|
1606
|
+
export function initDatalyr() {
|
|
1607
|
+
if (initialized || !browser) return;
|
|
1608
|
+
|
|
1609
|
+
datalyr.init({
|
|
1610
|
+
workspaceId: import.meta.env.PUBLIC_DATALYR_WORKSPACE_ID,
|
|
1611
|
+
debug: import.meta.env.DEV,
|
|
1612
|
+
trackSPA: true,
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
initialized = true;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
export { datalyr };
|
|
1619
|
+
\`\`\`
|
|
1620
|
+
|
|
1621
|
+
### 2. Initialize in Layout
|
|
1622
|
+
Update \`src/routes/+layout.svelte\`:
|
|
1623
|
+
\`\`\`svelte
|
|
1624
|
+
<script>
|
|
1625
|
+
import { onMount } from 'svelte';
|
|
1626
|
+
import { initDatalyr } from '$lib/datalyr';
|
|
1627
|
+
|
|
1628
|
+
onMount(() => {
|
|
1629
|
+
initDatalyr();
|
|
1630
|
+
});
|
|
1631
|
+
</script>
|
|
1632
|
+
|
|
1633
|
+
<slot />
|
|
1634
|
+
\`\`\`
|
|
1635
|
+
|
|
1636
|
+
### 3. Server-Side Instance
|
|
1637
|
+
Create \`src/lib/server/datalyr.ts\`:
|
|
1638
|
+
\`\`\`ts
|
|
1639
|
+
import Datalyr from '@datalyr/api';
|
|
1640
|
+
import { DATALYR_API_KEY } from '$env/static/private';
|
|
1641
|
+
|
|
1642
|
+
export const datalyr = new Datalyr(DATALYR_API_KEY);
|
|
1643
|
+
\`\`\`
|
|
1644
|
+
`;
|
|
1645
|
+
}
|
|
1646
|
+
function getSvelteDocs(apiKey) {
|
|
1647
|
+
return `
|
|
1648
|
+
# Datalyr Svelte Integration
|
|
1649
|
+
|
|
1650
|
+
## Package to Install
|
|
1651
|
+
- @datalyr/web
|
|
1652
|
+
|
|
1653
|
+
## Environment Variables
|
|
1654
|
+
Add to .env:
|
|
1655
|
+
\`\`\`
|
|
1656
|
+
VITE_DATALYR_WORKSPACE_ID=your_workspace_id
|
|
1657
|
+
\`\`\`
|
|
1658
|
+
|
|
1659
|
+
## Setup
|
|
1660
|
+
|
|
1661
|
+
### 1. Create Initialization Module
|
|
1662
|
+
Create \`src/lib/datalyr.ts\`:
|
|
1663
|
+
\`\`\`ts
|
|
1664
|
+
import datalyr from '@datalyr/web';
|
|
1665
|
+
|
|
1666
|
+
let initialized = false;
|
|
1667
|
+
|
|
1668
|
+
export function initDatalyr() {
|
|
1669
|
+
if (initialized || typeof window === 'undefined') return;
|
|
1670
|
+
|
|
1671
|
+
datalyr.init({
|
|
1672
|
+
workspaceId: import.meta.env.VITE_DATALYR_WORKSPACE_ID,
|
|
1673
|
+
debug: import.meta.env.DEV,
|
|
1674
|
+
trackSPA: true,
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
initialized = true;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
export { datalyr };
|
|
1681
|
+
\`\`\`
|
|
1682
|
+
|
|
1683
|
+
### 2. Initialize in App
|
|
1684
|
+
Update \`src/App.svelte\`:
|
|
1685
|
+
\`\`\`svelte
|
|
1686
|
+
<script>
|
|
1687
|
+
import { onMount } from 'svelte';
|
|
1688
|
+
import { initDatalyr } from './lib/datalyr';
|
|
1689
|
+
|
|
1690
|
+
onMount(() => {
|
|
1691
|
+
initDatalyr();
|
|
1692
|
+
});
|
|
1693
|
+
</script>
|
|
1694
|
+
|
|
1695
|
+
<!-- Your app content -->
|
|
1696
|
+
\`\`\`
|
|
1697
|
+
`;
|
|
1698
|
+
}
|
|
1699
|
+
function getReactNativeDocs(_apiKey, isExpo) {
|
|
1700
|
+
const importPath = isExpo ? "@datalyr/react-native/expo" : "@datalyr/react-native";
|
|
1701
|
+
const configImport = isExpo ? "import Constants from 'expo-constants';" : "";
|
|
1702
|
+
const configAccess = isExpo ? "Constants.expoConfig?.extra?.datalyrApiKey || ''" : "process.env.DATALYR_API_KEY || ''";
|
|
1703
|
+
return `
|
|
1704
|
+
# Datalyr React Native Integration${isExpo ? " (Expo)" : ""}
|
|
1705
|
+
|
|
1706
|
+
## Package to Install
|
|
1707
|
+
- @datalyr/react-native
|
|
1708
|
+
${isExpo ? "- expo-constants (for config access)" : ""}
|
|
1709
|
+
|
|
1710
|
+
## Post-Install (iOS)
|
|
1711
|
+
After installing, run:
|
|
1712
|
+
\`\`\`bash
|
|
1713
|
+
cd ios && pod install
|
|
1714
|
+
\`\`\`
|
|
1715
|
+
|
|
1716
|
+
## Environment Configuration
|
|
1717
|
+
${isExpo ? `
|
|
1718
|
+
### For Expo
|
|
1719
|
+
Add to \`app.config.js\` or \`app.json\`:
|
|
1720
|
+
\`\`\`js
|
|
1721
|
+
export default {
|
|
1722
|
+
expo: {
|
|
1723
|
+
extra: {
|
|
1724
|
+
datalyrApiKey: process.env.DATALYR_API_KEY,
|
|
1725
|
+
datalyrWorkspaceId: process.env.DATALYR_WORKSPACE_ID,
|
|
1726
|
+
},
|
|
1727
|
+
},
|
|
1728
|
+
};
|
|
1729
|
+
\`\`\`
|
|
1730
|
+
|
|
1731
|
+
Create \`.env\` file:
|
|
1732
|
+
\`\`\`
|
|
1733
|
+
DATALYR_API_KEY=your_api_key
|
|
1734
|
+
DATALYR_WORKSPACE_ID=your_workspace_id
|
|
1735
|
+
\`\`\`
|
|
1736
|
+
` : `
|
|
1737
|
+
### For React Native CLI
|
|
1738
|
+
Create \`.env\` file and use react-native-config:
|
|
1739
|
+
\`\`\`
|
|
1740
|
+
DATALYR_API_KEY=your_api_key
|
|
1741
|
+
DATALYR_WORKSPACE_ID=your_workspace_id
|
|
1742
|
+
\`\`\`
|
|
1743
|
+
`}
|
|
1744
|
+
**Security Note**: Never commit API keys to source control. Use environment variables or secrets management.
|
|
1745
|
+
|
|
1746
|
+
## Setup
|
|
1747
|
+
|
|
1748
|
+
### 1. Create Initialization Module
|
|
1749
|
+
Create \`src/utils/datalyr.ts\`:
|
|
1750
|
+
\`\`\`ts
|
|
1751
|
+
import { Datalyr } from '${importPath}';
|
|
1752
|
+
${configImport}
|
|
1753
|
+
|
|
1754
|
+
let initialized = false;
|
|
1755
|
+
|
|
1756
|
+
export async function initDatalyr() {
|
|
1757
|
+
if (initialized) return;
|
|
1758
|
+
|
|
1759
|
+
const apiKey = ${configAccess};
|
|
1760
|
+
|
|
1761
|
+
if (!apiKey) {
|
|
1762
|
+
console.warn('[Datalyr] API key not configured');
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
await Datalyr.initialize({
|
|
1767
|
+
apiKey,
|
|
1768
|
+
enableAutoEvents: true,
|
|
1769
|
+
enableAttribution: true,
|
|
1770
|
+
debug: __DEV__,
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
initialized = true;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
export { Datalyr };
|
|
1777
|
+
\`\`\`
|
|
1778
|
+
|
|
1779
|
+
### 2. Initialize in App
|
|
1780
|
+
Update \`App.tsx\`:
|
|
1781
|
+
\`\`\`tsx
|
|
1782
|
+
import { useEffect } from 'react';
|
|
1783
|
+
import { initDatalyr } from './src/utils/datalyr';
|
|
1784
|
+
|
|
1785
|
+
export default function App() {
|
|
1786
|
+
useEffect(() => {
|
|
1787
|
+
initDatalyr();
|
|
1788
|
+
}, []);
|
|
1789
|
+
|
|
1790
|
+
return (
|
|
1791
|
+
// Your app content
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
\`\`\`
|
|
1795
|
+
|
|
1796
|
+
## Tracking Events
|
|
1797
|
+
\`\`\`ts
|
|
1798
|
+
import { Datalyr } from './src/utils/datalyr';
|
|
1799
|
+
|
|
1800
|
+
// Track an event
|
|
1801
|
+
Datalyr.track('purchase_completed', { amount: 99.99 });
|
|
1802
|
+
|
|
1803
|
+
// Identify a user
|
|
1804
|
+
Datalyr.identify('user_123', { email: 'user@example.com' });
|
|
1805
|
+
\`\`\`
|
|
1806
|
+
`;
|
|
1807
|
+
}
|
|
1808
|
+
function getNodeDocs(apiKey) {
|
|
1809
|
+
return `
|
|
1810
|
+
# Datalyr Node.js Integration
|
|
1811
|
+
|
|
1812
|
+
## Package to Install
|
|
1813
|
+
- @datalyr/api
|
|
1814
|
+
|
|
1815
|
+
## Environment Variables
|
|
1816
|
+
Add to .env:
|
|
1817
|
+
\`\`\`
|
|
1818
|
+
DATALYR_API_KEY=${apiKey}
|
|
1819
|
+
\`\`\`
|
|
1820
|
+
|
|
1821
|
+
## Setup
|
|
1822
|
+
|
|
1823
|
+
### 1. Create Datalyr Instance
|
|
1824
|
+
Create \`src/lib/datalyr.ts\`:
|
|
1825
|
+
\`\`\`ts
|
|
1826
|
+
import Datalyr from '@datalyr/api';
|
|
1827
|
+
|
|
1828
|
+
const apiKey = process.env.DATALYR_API_KEY;
|
|
1829
|
+
if (!apiKey) {
|
|
1830
|
+
throw new Error('DATALYR_API_KEY is required');
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
export const datalyr = new Datalyr(apiKey);
|
|
1834
|
+
\`\`\`
|
|
1835
|
+
|
|
1836
|
+
### 2. Track Events
|
|
1837
|
+
\`\`\`ts
|
|
1838
|
+
import { datalyr } from './lib/datalyr';
|
|
1839
|
+
|
|
1840
|
+
// Track server-side events
|
|
1841
|
+
await datalyr.track({
|
|
1842
|
+
event: 'order_completed',
|
|
1843
|
+
userId: 'user_123',
|
|
1844
|
+
properties: {
|
|
1845
|
+
orderId: 'order_456',
|
|
1846
|
+
amount: 99.99,
|
|
1847
|
+
},
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
// Identify a user
|
|
1851
|
+
await datalyr.identify({
|
|
1852
|
+
userId: 'user_123',
|
|
1853
|
+
traits: {
|
|
1854
|
+
email: 'user@example.com',
|
|
1855
|
+
plan: 'premium',
|
|
1856
|
+
},
|
|
1857
|
+
});
|
|
1858
|
+
\`\`\`
|
|
1859
|
+
`;
|
|
1860
|
+
}
|
|
1861
|
+
function getIOSDocs(_apiKey) {
|
|
1862
|
+
return `
|
|
1863
|
+
# Datalyr iOS (Swift) Integration
|
|
1864
|
+
|
|
1865
|
+
## Package to Install
|
|
1866
|
+
Add via Swift Package Manager:
|
|
1867
|
+
- https://github.com/datalyr/datalyr-ios-sdk
|
|
1868
|
+
|
|
1869
|
+
## Setup
|
|
1870
|
+
|
|
1871
|
+
### 1. Add Keys to Info.plist
|
|
1872
|
+
Add both keys to your Info.plist (or use xcconfig for different environments):
|
|
1873
|
+
\`\`\`xml
|
|
1874
|
+
<key>DATALYR_WORKSPACE_ID</key>
|
|
1875
|
+
<string>YOUR_WORKSPACE_ID</string>
|
|
1876
|
+
<key>DATALYR_API_KEY</key>
|
|
1877
|
+
<string>YOUR_API_KEY</string>
|
|
1878
|
+
\`\`\`
|
|
1879
|
+
|
|
1880
|
+
**Security Note**: For production apps, consider using xcconfig files or build-time environment variables to inject these values, keeping them out of source control.
|
|
1881
|
+
|
|
1882
|
+
### 2. Add Configuration File
|
|
1883
|
+
Create \`DatalyrConfig.swift\`:
|
|
1884
|
+
\`\`\`swift
|
|
1885
|
+
import DatalyrSDK
|
|
1886
|
+
import Foundation
|
|
1887
|
+
|
|
1888
|
+
struct DatalyrConfig {
|
|
1889
|
+
static func initialize() {
|
|
1890
|
+
guard let workspaceId = Bundle.main.object(forInfoDictionaryKey: "DATALYR_WORKSPACE_ID") as? String,
|
|
1891
|
+
let apiKey = Bundle.main.object(forInfoDictionaryKey: "DATALYR_API_KEY") as? String else {
|
|
1892
|
+
#if DEBUG
|
|
1893
|
+
fatalError("DATALYR_WORKSPACE_ID or DATALYR_API_KEY not found in Info.plist")
|
|
1894
|
+
#else
|
|
1895
|
+
print("[Datalyr] Warning: SDK not configured - missing Info.plist keys")
|
|
1896
|
+
return
|
|
1897
|
+
#endif
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
Datalyr.shared.configure(
|
|
1901
|
+
apiKey: apiKey,
|
|
1902
|
+
workspaceId: workspaceId,
|
|
1903
|
+
options: DatalyrOptions(
|
|
1904
|
+
debug: false,
|
|
1905
|
+
enableAutoEvents: true,
|
|
1906
|
+
enableAttribution: true
|
|
1907
|
+
)
|
|
1908
|
+
)
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
\`\`\`
|
|
1912
|
+
|
|
1913
|
+
### 3. Initialize in App
|
|
1914
|
+
For SwiftUI (\`@main App\`):
|
|
1915
|
+
\`\`\`swift
|
|
1916
|
+
@main
|
|
1917
|
+
struct MyApp: App {
|
|
1918
|
+
init() {
|
|
1919
|
+
DatalyrConfig.initialize()
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
var body: some Scene {
|
|
1923
|
+
WindowGroup {
|
|
1924
|
+
ContentView()
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
\`\`\`
|
|
1929
|
+
|
|
1930
|
+
For UIKit (AppDelegate):
|
|
1931
|
+
\`\`\`swift
|
|
1932
|
+
func application(_ application: UIApplication, didFinishLaunchingWithOptions...) -> Bool {
|
|
1933
|
+
DatalyrConfig.initialize()
|
|
1934
|
+
return true
|
|
1935
|
+
}
|
|
1936
|
+
\`\`\`
|
|
1937
|
+
|
|
1938
|
+
## Tracking Events
|
|
1939
|
+
\`\`\`swift
|
|
1940
|
+
import DatalyrSDK
|
|
1941
|
+
|
|
1942
|
+
// Track an event
|
|
1943
|
+
Datalyr.shared.track("purchase_completed", properties: [
|
|
1944
|
+
"amount": 99.99,
|
|
1945
|
+
"currency": "USD"
|
|
1946
|
+
])
|
|
1947
|
+
|
|
1948
|
+
// Identify a user
|
|
1949
|
+
Datalyr.shared.identify("user_123", traits: [
|
|
1950
|
+
"email": "user@example.com",
|
|
1951
|
+
"plan": "premium"
|
|
1952
|
+
])
|
|
1953
|
+
\`\`\`
|
|
1954
|
+
`;
|
|
1955
|
+
}
|
|
1956
|
+
function getGenericWebDocs(apiKey) {
|
|
1957
|
+
return `
|
|
1958
|
+
# Datalyr Web Integration
|
|
1959
|
+
|
|
1960
|
+
## Package to Install
|
|
1961
|
+
- @datalyr/web
|
|
1962
|
+
|
|
1963
|
+
## Setup
|
|
1964
|
+
|
|
1965
|
+
### Via NPM
|
|
1966
|
+
\`\`\`ts
|
|
1967
|
+
import datalyr from '@datalyr/web';
|
|
1968
|
+
|
|
1969
|
+
datalyr.init({
|
|
1970
|
+
workspaceId: 'YOUR_WORKSPACE_ID',
|
|
1971
|
+
debug: true,
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
// Track events
|
|
1975
|
+
datalyr.track('page_viewed', { page: '/home' });
|
|
1976
|
+
\`\`\`
|
|
1977
|
+
|
|
1978
|
+
### Via Script Tag
|
|
1979
|
+
\`\`\`html
|
|
1980
|
+
<script src="https://track.datalyr.com/dl.js"
|
|
1981
|
+
data-workspace-id="YOUR_WORKSPACE_ID">
|
|
1982
|
+
</script>
|
|
1983
|
+
\`\`\`
|
|
1984
|
+
`;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// src/agent/configs/types.ts
|
|
1988
|
+
var NEXTJS_CONFIG = {
|
|
1989
|
+
id: "nextjs",
|
|
1990
|
+
name: "Next.js",
|
|
1991
|
+
detectPackage: "next",
|
|
1992
|
+
sdks: ["@datalyr/web", "@datalyr/api"],
|
|
1993
|
+
envVars: [
|
|
1994
|
+
{
|
|
1995
|
+
key: "NEXT_PUBLIC_DATALYR_WORKSPACE_ID",
|
|
1996
|
+
description: "Your Datalyr workspace ID",
|
|
1997
|
+
isPublic: true
|
|
1998
|
+
},
|
|
1999
|
+
{
|
|
2000
|
+
key: "DATALYR_API_KEY",
|
|
2001
|
+
description: "Server-side API key",
|
|
2002
|
+
isPublic: false
|
|
2003
|
+
}
|
|
2004
|
+
],
|
|
2005
|
+
estimatedTime: 3,
|
|
2006
|
+
docsUrl: "https://docs.datalyr.com/sdks/nextjs",
|
|
2007
|
+
postInstallSteps: [
|
|
2008
|
+
"Add your workspace ID to .env.local",
|
|
2009
|
+
"Wrap your layout with DatalyrProvider",
|
|
2010
|
+
"Start tracking events with datalyr.track()"
|
|
2011
|
+
],
|
|
2012
|
+
routerDetection: {
|
|
2013
|
+
appRouter: ["app/layout.tsx", "app/layout.jsx", "app/layout.ts", "app/layout.js"],
|
|
2014
|
+
pagesRouter: ["pages/_app.tsx", "pages/_app.jsx", "pages/_app.ts", "pages/_app.js"]
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
var REACT_CONFIG = {
|
|
2018
|
+
id: "react",
|
|
2019
|
+
name: "React",
|
|
2020
|
+
detectPackage: "react",
|
|
2021
|
+
sdks: ["@datalyr/web"],
|
|
2022
|
+
envVars: [
|
|
2023
|
+
{
|
|
2024
|
+
key: "VITE_DATALYR_WORKSPACE_ID",
|
|
2025
|
+
description: "Your Datalyr workspace ID",
|
|
2026
|
+
isPublic: true
|
|
2027
|
+
}
|
|
2028
|
+
],
|
|
2029
|
+
estimatedTime: 2,
|
|
2030
|
+
docsUrl: "https://docs.datalyr.com/sdks/react",
|
|
2031
|
+
postInstallSteps: [
|
|
2032
|
+
"Add your workspace ID to .env",
|
|
2033
|
+
"Call initDatalyr() in your main file"
|
|
2034
|
+
]
|
|
2035
|
+
};
|
|
2036
|
+
var REACT_NATIVE_CONFIG = {
|
|
2037
|
+
id: "react-native",
|
|
2038
|
+
name: "React Native",
|
|
2039
|
+
detectPackage: "react-native",
|
|
2040
|
+
sdks: ["@datalyr/react-native"],
|
|
2041
|
+
envVars: [],
|
|
2042
|
+
estimatedTime: 5,
|
|
2043
|
+
docsUrl: "https://docs.datalyr.com/sdks/react-native",
|
|
2044
|
+
postInstallSteps: [
|
|
2045
|
+
"Run: cd ios && pod install",
|
|
2046
|
+
"Call initDatalyr() in App.tsx",
|
|
2047
|
+
"For attribution, configure Meta/TikTok app IDs"
|
|
2048
|
+
]
|
|
2049
|
+
};
|
|
2050
|
+
var EXPO_CONFIG = {
|
|
2051
|
+
id: "expo",
|
|
2052
|
+
name: "Expo",
|
|
2053
|
+
detectPackage: "expo",
|
|
2054
|
+
sdks: ["@datalyr/react-native"],
|
|
2055
|
+
envVars: [],
|
|
2056
|
+
estimatedTime: 3,
|
|
2057
|
+
docsUrl: "https://docs.datalyr.com/sdks/expo",
|
|
2058
|
+
postInstallSteps: [
|
|
2059
|
+
"Call initDatalyr() in App.tsx",
|
|
2060
|
+
"For attribution, configure Meta/TikTok app IDs in app.json"
|
|
2061
|
+
]
|
|
2062
|
+
};
|
|
2063
|
+
var SVELTEKIT_CONFIG = {
|
|
2064
|
+
id: "sveltekit",
|
|
2065
|
+
name: "SvelteKit",
|
|
2066
|
+
detectPackage: "@sveltejs/kit",
|
|
2067
|
+
sdks: ["@datalyr/web", "@datalyr/api"],
|
|
2068
|
+
envVars: [
|
|
2069
|
+
{
|
|
2070
|
+
key: "PUBLIC_DATALYR_WORKSPACE_ID",
|
|
2071
|
+
description: "Your Datalyr workspace ID",
|
|
2072
|
+
isPublic: true
|
|
2073
|
+
},
|
|
2074
|
+
{
|
|
2075
|
+
key: "DATALYR_API_KEY",
|
|
2076
|
+
description: "Server-side API key",
|
|
2077
|
+
isPublic: false
|
|
2078
|
+
}
|
|
2079
|
+
],
|
|
2080
|
+
estimatedTime: 3,
|
|
2081
|
+
docsUrl: "https://docs.datalyr.com/sdks/sveltekit",
|
|
2082
|
+
postInstallSteps: [
|
|
2083
|
+
"Add your workspace ID to .env",
|
|
2084
|
+
"Call initDatalyr() in +layout.svelte"
|
|
2085
|
+
]
|
|
2086
|
+
};
|
|
2087
|
+
var NODE_CONFIG = {
|
|
2088
|
+
id: "node",
|
|
2089
|
+
name: "Node.js",
|
|
2090
|
+
detectPackage: "express",
|
|
2091
|
+
sdks: ["@datalyr/api"],
|
|
2092
|
+
envVars: [
|
|
2093
|
+
{
|
|
2094
|
+
key: "DATALYR_API_KEY",
|
|
2095
|
+
description: "Your Datalyr API key",
|
|
2096
|
+
isPublic: false
|
|
2097
|
+
}
|
|
2098
|
+
],
|
|
2099
|
+
estimatedTime: 2,
|
|
2100
|
+
docsUrl: "https://docs.datalyr.com/sdks/node",
|
|
2101
|
+
postInstallSteps: [
|
|
2102
|
+
"Add your API key to .env",
|
|
2103
|
+
"Track events with datalyr.track()"
|
|
2104
|
+
]
|
|
2105
|
+
};
|
|
2106
|
+
var VUE_CONFIG = {
|
|
2107
|
+
id: "vue",
|
|
2108
|
+
name: "Vue.js",
|
|
2109
|
+
detectPackage: "vue",
|
|
2110
|
+
sdks: ["@datalyr/web"],
|
|
2111
|
+
envVars: [
|
|
2112
|
+
{
|
|
2113
|
+
key: "VITE_DATALYR_WORKSPACE_ID",
|
|
2114
|
+
description: "Your Datalyr workspace ID",
|
|
2115
|
+
isPublic: true
|
|
2116
|
+
}
|
|
2117
|
+
],
|
|
2118
|
+
estimatedTime: 2,
|
|
2119
|
+
docsUrl: "https://docs.datalyr.com/sdks/vue",
|
|
2120
|
+
postInstallSteps: [
|
|
2121
|
+
"Add your workspace ID to .env",
|
|
2122
|
+
"Call datalyr.init() in main.ts",
|
|
2123
|
+
"Track events with datalyr.track()"
|
|
2124
|
+
]
|
|
2125
|
+
};
|
|
2126
|
+
var NUXT_CONFIG = {
|
|
2127
|
+
id: "nuxt",
|
|
2128
|
+
name: "Nuxt",
|
|
2129
|
+
detectPackage: "nuxt",
|
|
2130
|
+
sdks: ["@datalyr/web", "@datalyr/api"],
|
|
2131
|
+
envVars: [
|
|
2132
|
+
{
|
|
2133
|
+
key: "NUXT_PUBLIC_DATALYR_WORKSPACE_ID",
|
|
2134
|
+
description: "Your Datalyr workspace ID",
|
|
2135
|
+
isPublic: true
|
|
2136
|
+
},
|
|
2137
|
+
{
|
|
2138
|
+
key: "DATALYR_API_KEY",
|
|
2139
|
+
description: "Server-side API key",
|
|
2140
|
+
isPublic: false
|
|
2141
|
+
}
|
|
2142
|
+
],
|
|
2143
|
+
estimatedTime: 3,
|
|
2144
|
+
docsUrl: "https://docs.datalyr.com/sdks/nuxt",
|
|
2145
|
+
postInstallSteps: [
|
|
2146
|
+
"Add your workspace ID to .env",
|
|
2147
|
+
"Create a Nuxt plugin for initialization",
|
|
2148
|
+
"Track events with datalyr.track()"
|
|
2149
|
+
]
|
|
2150
|
+
};
|
|
2151
|
+
var REMIX_CONFIG = {
|
|
2152
|
+
id: "remix",
|
|
2153
|
+
name: "Remix",
|
|
2154
|
+
detectPackage: "@remix-run/react",
|
|
2155
|
+
sdks: ["@datalyr/web", "@datalyr/api"],
|
|
2156
|
+
envVars: [
|
|
2157
|
+
{
|
|
2158
|
+
key: "DATALYR_WORKSPACE_ID",
|
|
2159
|
+
description: "Your Datalyr workspace ID",
|
|
2160
|
+
isPublic: true
|
|
2161
|
+
},
|
|
2162
|
+
{
|
|
2163
|
+
key: "DATALYR_API_KEY",
|
|
2164
|
+
description: "Server-side API key",
|
|
2165
|
+
isPublic: false
|
|
2166
|
+
}
|
|
2167
|
+
],
|
|
2168
|
+
estimatedTime: 3,
|
|
2169
|
+
docsUrl: "https://docs.datalyr.com/sdks/remix",
|
|
2170
|
+
postInstallSteps: [
|
|
2171
|
+
"Add your workspace ID to .env",
|
|
2172
|
+
"Initialize in root.tsx",
|
|
2173
|
+
"Track events with datalyr.track()"
|
|
2174
|
+
]
|
|
2175
|
+
};
|
|
2176
|
+
var ASTRO_CONFIG = {
|
|
2177
|
+
id: "astro",
|
|
2178
|
+
name: "Astro",
|
|
2179
|
+
detectPackage: "astro",
|
|
2180
|
+
sdks: ["@datalyr/web"],
|
|
2181
|
+
envVars: [
|
|
2182
|
+
{
|
|
2183
|
+
key: "PUBLIC_DATALYR_WORKSPACE_ID",
|
|
2184
|
+
description: "Your Datalyr workspace ID",
|
|
2185
|
+
isPublic: true
|
|
2186
|
+
}
|
|
2187
|
+
],
|
|
2188
|
+
estimatedTime: 2,
|
|
2189
|
+
docsUrl: "https://docs.datalyr.com/sdks/astro",
|
|
2190
|
+
postInstallSteps: [
|
|
2191
|
+
"Add your workspace ID to .env",
|
|
2192
|
+
"Add script to Layout component",
|
|
2193
|
+
"Track events with datalyr.track()"
|
|
2194
|
+
]
|
|
2195
|
+
};
|
|
2196
|
+
var IOS_CONFIG = {
|
|
2197
|
+
id: "ios",
|
|
2198
|
+
name: "iOS (Swift)",
|
|
2199
|
+
detectPackage: "",
|
|
2200
|
+
// Detected by Package.swift or .xcodeproj
|
|
2201
|
+
sdks: ["DatalyrSDK"],
|
|
2202
|
+
envVars: [
|
|
2203
|
+
{
|
|
2204
|
+
key: "DATALYR_WORKSPACE_ID",
|
|
2205
|
+
description: "Your Datalyr workspace ID (in Info.plist)",
|
|
2206
|
+
isPublic: false
|
|
2207
|
+
},
|
|
2208
|
+
{
|
|
2209
|
+
key: "DATALYR_API_KEY",
|
|
2210
|
+
description: "Your Datalyr API key (in Info.plist)",
|
|
2211
|
+
isPublic: false
|
|
2212
|
+
}
|
|
2213
|
+
],
|
|
2214
|
+
estimatedTime: 5,
|
|
2215
|
+
docsUrl: "https://docs.datalyr.com/sdks/ios",
|
|
2216
|
+
postInstallSteps: [
|
|
2217
|
+
"Add DatalyrSDK via Swift Package Manager",
|
|
2218
|
+
"Add keys to Info.plist",
|
|
2219
|
+
"Call DatalyrConfig.initialize() in App init"
|
|
2220
|
+
]
|
|
2221
|
+
};
|
|
2222
|
+
var SVELTE_CONFIG = {
|
|
2223
|
+
id: "svelte",
|
|
2224
|
+
name: "Svelte",
|
|
2225
|
+
detectPackage: "svelte",
|
|
2226
|
+
sdks: ["@datalyr/web"],
|
|
2227
|
+
envVars: [
|
|
2228
|
+
{
|
|
2229
|
+
key: "VITE_DATALYR_WORKSPACE_ID",
|
|
2230
|
+
description: "Your Datalyr workspace ID",
|
|
2231
|
+
isPublic: true
|
|
2232
|
+
}
|
|
2233
|
+
],
|
|
2234
|
+
estimatedTime: 2,
|
|
2235
|
+
docsUrl: "https://docs.datalyr.com/sdks/svelte",
|
|
2236
|
+
postInstallSteps: [
|
|
2237
|
+
"Add your workspace ID to .env",
|
|
2238
|
+
"Call initDatalyr() in App.svelte"
|
|
2239
|
+
]
|
|
2240
|
+
};
|
|
2241
|
+
var FRAMEWORK_CONFIGS = {
|
|
2242
|
+
nextjs: NEXTJS_CONFIG,
|
|
2243
|
+
react: REACT_CONFIG,
|
|
2244
|
+
"react-vite": REACT_CONFIG,
|
|
2245
|
+
svelte: SVELTE_CONFIG,
|
|
2246
|
+
sveltekit: SVELTEKIT_CONFIG,
|
|
2247
|
+
vue: VUE_CONFIG,
|
|
2248
|
+
nuxt: NUXT_CONFIG,
|
|
2249
|
+
remix: REMIX_CONFIG,
|
|
2250
|
+
astro: ASTRO_CONFIG,
|
|
2251
|
+
"react-native": REACT_NATIVE_CONFIG,
|
|
2252
|
+
expo: EXPO_CONFIG,
|
|
2253
|
+
ios: IOS_CONFIG,
|
|
2254
|
+
node: NODE_CONFIG,
|
|
2255
|
+
unknown: void 0
|
|
2256
|
+
};
|
|
2257
|
+
function getSdksForFramework(framework) {
|
|
2258
|
+
const config = FRAMEWORK_CONFIGS[framework];
|
|
2259
|
+
if (!config) {
|
|
2260
|
+
return ["@datalyr/web"];
|
|
2261
|
+
}
|
|
2262
|
+
return config.sdks;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// src/agent/events/suggestions.ts
|
|
2266
|
+
var BUSINESS_TYPES = [
|
|
2267
|
+
{
|
|
2268
|
+
id: "saas",
|
|
2269
|
+
label: "SaaS / Web App",
|
|
2270
|
+
description: "Subscription or freemium product",
|
|
2271
|
+
hint: "Trials, signups, features, upgrades"
|
|
2272
|
+
},
|
|
2273
|
+
{
|
|
2274
|
+
id: "mobile_app",
|
|
2275
|
+
label: "Mobile App",
|
|
2276
|
+
description: "Consumer or B2B mobile app",
|
|
2277
|
+
hint: "Installs, in-app events, attribution"
|
|
2278
|
+
},
|
|
2279
|
+
{
|
|
2280
|
+
id: "lead_gen",
|
|
2281
|
+
label: "Lead Gen / Marketing",
|
|
2282
|
+
description: "Capture leads and conversions",
|
|
2283
|
+
hint: "Funnels, forms, demos, signups"
|
|
2284
|
+
},
|
|
2285
|
+
{
|
|
2286
|
+
id: "b2b",
|
|
2287
|
+
label: "B2B Product",
|
|
2288
|
+
description: "Business software",
|
|
2289
|
+
hint: "Demos, trials, team invites, usage"
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
id: "agency",
|
|
2293
|
+
label: "Agency / Client Work",
|
|
2294
|
+
description: "Building for clients",
|
|
2295
|
+
hint: "Multi-site, white-label tracking"
|
|
2296
|
+
}
|
|
2297
|
+
];
|
|
2298
|
+
var EVENT_SUGGESTIONS = {
|
|
2299
|
+
saas: [
|
|
2300
|
+
{
|
|
2301
|
+
name: "signed_up",
|
|
2302
|
+
description: "User created an account",
|
|
2303
|
+
properties: ["method", "referrer"],
|
|
2304
|
+
priority: "high"
|
|
2305
|
+
},
|
|
2306
|
+
{
|
|
2307
|
+
name: "trial_started",
|
|
2308
|
+
description: "User started a free trial",
|
|
2309
|
+
properties: ["plan"],
|
|
2310
|
+
priority: "high"
|
|
2311
|
+
},
|
|
2312
|
+
{
|
|
2313
|
+
name: "subscription_started",
|
|
2314
|
+
description: "User converted to paid",
|
|
2315
|
+
properties: ["plan", "value", "currency"],
|
|
2316
|
+
priority: "high"
|
|
2317
|
+
},
|
|
2318
|
+
{
|
|
2319
|
+
name: "onboarding_completed",
|
|
2320
|
+
description: "User finished onboarding",
|
|
2321
|
+
properties: ["steps_completed"],
|
|
2322
|
+
priority: "medium"
|
|
2323
|
+
}
|
|
2324
|
+
],
|
|
2325
|
+
mobile_app: [
|
|
2326
|
+
{
|
|
2327
|
+
name: "signed_up",
|
|
2328
|
+
description: "User created an account",
|
|
2329
|
+
properties: ["method", "referrer"],
|
|
2330
|
+
priority: "high"
|
|
2331
|
+
},
|
|
2332
|
+
{
|
|
2333
|
+
name: "onboarding_started",
|
|
2334
|
+
description: "User started onboarding flow",
|
|
2335
|
+
properties: ["source"],
|
|
2336
|
+
priority: "high"
|
|
2337
|
+
},
|
|
2338
|
+
{
|
|
2339
|
+
name: "onboarding_completed",
|
|
2340
|
+
description: "User finished onboarding",
|
|
2341
|
+
properties: ["steps_completed"],
|
|
2342
|
+
priority: "high"
|
|
2343
|
+
},
|
|
2344
|
+
{
|
|
2345
|
+
name: "paywall_viewed",
|
|
2346
|
+
description: "User saw the paywall/pricing",
|
|
2347
|
+
properties: ["source", "paywall_id"],
|
|
2348
|
+
priority: "high"
|
|
2349
|
+
},
|
|
2350
|
+
{
|
|
2351
|
+
name: "trial_started",
|
|
2352
|
+
description: "User started a free trial",
|
|
2353
|
+
properties: ["plan"],
|
|
2354
|
+
priority: "high"
|
|
2355
|
+
},
|
|
2356
|
+
{
|
|
2357
|
+
name: "subscription_started",
|
|
2358
|
+
description: "User converted to paid subscription",
|
|
2359
|
+
properties: ["plan", "value", "currency"],
|
|
2360
|
+
priority: "high"
|
|
2361
|
+
},
|
|
2362
|
+
{
|
|
2363
|
+
name: "purchase",
|
|
2364
|
+
description: "User made in-app purchase",
|
|
2365
|
+
properties: ["product_id", "value", "currency"],
|
|
2366
|
+
priority: "medium"
|
|
2367
|
+
}
|
|
2368
|
+
],
|
|
2369
|
+
lead_gen: [
|
|
2370
|
+
{
|
|
2371
|
+
name: "lead",
|
|
2372
|
+
description: "User submitted their info",
|
|
2373
|
+
properties: ["form_name", "source"],
|
|
2374
|
+
priority: "high"
|
|
2375
|
+
},
|
|
2376
|
+
{
|
|
2377
|
+
name: "signed_up",
|
|
2378
|
+
description: "User signed up / joined waitlist",
|
|
2379
|
+
properties: ["method", "referrer"],
|
|
2380
|
+
priority: "high"
|
|
2381
|
+
},
|
|
2382
|
+
{
|
|
2383
|
+
name: "demo_requested",
|
|
2384
|
+
description: "User requested a demo",
|
|
2385
|
+
properties: ["product"],
|
|
2386
|
+
priority: "high"
|
|
2387
|
+
},
|
|
2388
|
+
{
|
|
2389
|
+
name: "cta_clicked",
|
|
2390
|
+
description: "User clicked a CTA",
|
|
2391
|
+
properties: ["button_name", "page"],
|
|
2392
|
+
priority: "medium"
|
|
2393
|
+
}
|
|
2394
|
+
],
|
|
2395
|
+
b2b: [
|
|
2396
|
+
{
|
|
2397
|
+
name: "signed_up",
|
|
2398
|
+
description: "User created an account",
|
|
2399
|
+
properties: ["method", "company_size"],
|
|
2400
|
+
priority: "high"
|
|
2401
|
+
},
|
|
2402
|
+
{
|
|
2403
|
+
name: "demo_requested",
|
|
2404
|
+
description: "User requested a demo",
|
|
2405
|
+
properties: ["product", "company_size"],
|
|
2406
|
+
priority: "high"
|
|
2407
|
+
},
|
|
2408
|
+
{
|
|
2409
|
+
name: "trial_started",
|
|
2410
|
+
description: "User started a trial",
|
|
2411
|
+
properties: ["plan"],
|
|
2412
|
+
priority: "high"
|
|
2413
|
+
},
|
|
2414
|
+
{
|
|
2415
|
+
name: "lead",
|
|
2416
|
+
description: "Lead captured",
|
|
2417
|
+
properties: ["source", "company_size"],
|
|
2418
|
+
priority: "high"
|
|
2419
|
+
}
|
|
2420
|
+
],
|
|
2421
|
+
agency: [
|
|
2422
|
+
{
|
|
2423
|
+
name: "lead",
|
|
2424
|
+
description: "Lead captured",
|
|
2425
|
+
properties: ["form_name", "client_id"],
|
|
2426
|
+
priority: "high"
|
|
2427
|
+
},
|
|
2428
|
+
{
|
|
2429
|
+
name: "conversion",
|
|
2430
|
+
description: "Conversion event",
|
|
2431
|
+
properties: ["conversion_type", "value", "client_id"],
|
|
2432
|
+
priority: "high"
|
|
2433
|
+
},
|
|
2434
|
+
{
|
|
2435
|
+
name: "form_submitted",
|
|
2436
|
+
description: "User submitted a form",
|
|
2437
|
+
properties: ["form_name", "client_id"],
|
|
2438
|
+
priority: "high"
|
|
2439
|
+
},
|
|
2440
|
+
{
|
|
2441
|
+
name: "cta_clicked",
|
|
2442
|
+
description: "User clicked a CTA",
|
|
2443
|
+
properties: ["button_name", "client_id"],
|
|
2444
|
+
priority: "medium"
|
|
2445
|
+
}
|
|
2446
|
+
]
|
|
2447
|
+
};
|
|
2448
|
+
function getEventSuggestions(businessType) {
|
|
2449
|
+
return EVENT_SUGGESTIONS[businessType] || EVENT_SUGGESTIONS.saas;
|
|
2450
|
+
}
|
|
2451
|
+
function formatEventDescription(event) {
|
|
2452
|
+
const propsDisplay = event.properties.length > 0 ? ` (${event.properties.join(", ")})` : "";
|
|
2453
|
+
return `${event.description}${propsDisplay}`;
|
|
2454
|
+
}
|
|
2455
|
+
function buildEventSelectOptions(events, includeAllOption = true) {
|
|
2456
|
+
const options = [];
|
|
2457
|
+
if (includeAllOption) {
|
|
2458
|
+
const highPriorityCount = events.filter((e) => e.priority === "high").length;
|
|
2459
|
+
options.push({
|
|
2460
|
+
value: "__all_recommended__",
|
|
2461
|
+
label: "All recommended events",
|
|
2462
|
+
hint: `Select all ${highPriorityCount} high-priority events`
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
for (const event of events) {
|
|
2466
|
+
const priorityBadge = event.priority === "high" ? "[recommended] " : "";
|
|
2467
|
+
options.push({
|
|
2468
|
+
value: event.name,
|
|
2469
|
+
label: event.name,
|
|
2470
|
+
hint: `${priorityBadge}${formatEventDescription(event)}`
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
return options;
|
|
2474
|
+
}
|
|
2475
|
+
function resolveSelectedEvents(selectedValues, allEvents) {
|
|
2476
|
+
if (selectedValues.includes("__all_recommended__")) {
|
|
2477
|
+
return allEvents.filter((e) => e.priority === "high");
|
|
2478
|
+
}
|
|
2479
|
+
return allEvents.filter((e) => selectedValues.includes(e.name));
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// src/agent/platform/config.ts
|
|
2483
|
+
var PLATFORM_TYPES = [
|
|
2484
|
+
{
|
|
2485
|
+
id: "web",
|
|
2486
|
+
label: "Web Only",
|
|
2487
|
+
description: "Website or web application",
|
|
2488
|
+
hint: "React, Next.js, Svelte, etc."
|
|
2489
|
+
},
|
|
2490
|
+
{
|
|
2491
|
+
id: "mobile",
|
|
2492
|
+
label: "Mobile Only",
|
|
2493
|
+
description: "iOS or Android app",
|
|
2494
|
+
hint: "React Native, Expo, Swift"
|
|
2495
|
+
},
|
|
2496
|
+
{
|
|
2497
|
+
id: "both",
|
|
2498
|
+
label: "Web + Mobile",
|
|
2499
|
+
description: "Both web and mobile apps",
|
|
2500
|
+
hint: "Full cross-platform attribution"
|
|
2501
|
+
}
|
|
2502
|
+
];
|
|
2503
|
+
var AD_PLATFORMS = [
|
|
2504
|
+
{
|
|
2505
|
+
id: "meta",
|
|
2506
|
+
label: "Meta Ads (Facebook/Instagram)",
|
|
2507
|
+
description: "Track conversions from Meta campaigns",
|
|
2508
|
+
requiresServerSide: true,
|
|
2509
|
+
configKeys: ["META_PIXEL_ID", "META_ACCESS_TOKEN", "META_APP_ID"]
|
|
2510
|
+
},
|
|
2511
|
+
{
|
|
2512
|
+
id: "google",
|
|
2513
|
+
label: "Google Ads",
|
|
2514
|
+
description: "Track conversions from Google campaigns",
|
|
2515
|
+
requiresServerSide: true,
|
|
2516
|
+
configKeys: ["GOOGLE_ADS_CUSTOMER_ID", "GOOGLE_ADS_CONVERSION_ID"]
|
|
2517
|
+
},
|
|
2518
|
+
{
|
|
2519
|
+
id: "tiktok",
|
|
2520
|
+
label: "TikTok Ads",
|
|
2521
|
+
description: "Track conversions from TikTok campaigns",
|
|
2522
|
+
requiresServerSide: true,
|
|
2523
|
+
configKeys: ["TIKTOK_PIXEL_ID", "TIKTOK_ACCESS_TOKEN", "TIKTOK_APP_ID"]
|
|
2524
|
+
},
|
|
2525
|
+
{
|
|
2526
|
+
id: "apple_search_ads",
|
|
2527
|
+
label: "Apple Search Ads",
|
|
2528
|
+
description: "Track iOS app install attribution",
|
|
2529
|
+
requiresServerSide: false,
|
|
2530
|
+
configKeys: []
|
|
2531
|
+
},
|
|
2532
|
+
{
|
|
2533
|
+
id: "none",
|
|
2534
|
+
label: "No ad platforms",
|
|
2535
|
+
description: "Skip ad platform setup",
|
|
2536
|
+
requiresServerSide: false,
|
|
2537
|
+
configKeys: []
|
|
2538
|
+
}
|
|
2539
|
+
];
|
|
2540
|
+
function getSdksForPlatform(platformType, adPlatforms) {
|
|
2541
|
+
const sdks = [];
|
|
2542
|
+
if (platformType === "web" || platformType === "both") {
|
|
2543
|
+
sdks.push("@datalyr/web");
|
|
2544
|
+
}
|
|
2545
|
+
if (platformType === "mobile" || platformType === "both") {
|
|
2546
|
+
sdks.push("@datalyr/react-native");
|
|
2547
|
+
}
|
|
2548
|
+
const needsServerSide = adPlatforms.some(
|
|
2549
|
+
(p2) => AD_PLATFORMS.find((ap) => ap.id === p2)?.requiresServerSide
|
|
2550
|
+
);
|
|
2551
|
+
if (needsServerSide) {
|
|
2552
|
+
sdks.push("@datalyr/api");
|
|
2553
|
+
}
|
|
2554
|
+
return sdks;
|
|
2555
|
+
}
|
|
2556
|
+
function getAttributionEvents(config) {
|
|
2557
|
+
const events = [];
|
|
2558
|
+
if (config.platformType === "mobile" || config.platformType === "both") {
|
|
2559
|
+
events.push({
|
|
2560
|
+
name: "app_install",
|
|
2561
|
+
description: "User installed the app (auto-tracked)",
|
|
2562
|
+
properties: ["attribution_source", "campaign_id", "ad_group_id"],
|
|
2563
|
+
serverSide: false
|
|
2564
|
+
});
|
|
2565
|
+
events.push({
|
|
2566
|
+
name: "app_open",
|
|
2567
|
+
description: "User opened the app",
|
|
2568
|
+
properties: ["source", "deep_link_url", "is_first_open"],
|
|
2569
|
+
serverSide: false
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
if (config.enableDeepLinking) {
|
|
2573
|
+
events.push({
|
|
2574
|
+
name: "deep_link_opened",
|
|
2575
|
+
description: "User opened a deep link",
|
|
2576
|
+
properties: ["url", "source", "campaign"],
|
|
2577
|
+
serverSide: false
|
|
2578
|
+
});
|
|
2579
|
+
events.push({
|
|
2580
|
+
name: "deferred_deep_link",
|
|
2581
|
+
description: "User installed via deferred deep link",
|
|
2582
|
+
properties: ["original_url", "install_time", "open_time"],
|
|
2583
|
+
serverSide: false
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
if (config.enableServerSideConversions) {
|
|
2587
|
+
if (config.adPlatforms.includes("meta")) {
|
|
2588
|
+
events.push({
|
|
2589
|
+
name: "purchase",
|
|
2590
|
+
description: "Purchase event for Meta CAPI",
|
|
2591
|
+
properties: ["value", "currency", "email", "phone", "fbc", "fbp"],
|
|
2592
|
+
serverSide: true
|
|
2593
|
+
});
|
|
2594
|
+
events.push({
|
|
2595
|
+
name: "lead",
|
|
2596
|
+
description: "Lead event for Meta CAPI",
|
|
2597
|
+
properties: ["email", "phone", "fbc", "fbp"],
|
|
2598
|
+
serverSide: true
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
if (config.adPlatforms.includes("tiktok")) {
|
|
2602
|
+
events.push({
|
|
2603
|
+
name: "complete_payment",
|
|
2604
|
+
description: "Purchase event for TikTok Events API",
|
|
2605
|
+
properties: ["value", "currency", "email", "phone", "ttclid"],
|
|
2606
|
+
serverSide: true
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
if (config.adPlatforms.includes("google")) {
|
|
2610
|
+
events.push({
|
|
2611
|
+
name: "conversion",
|
|
2612
|
+
description: "Conversion event for Google Ads",
|
|
2613
|
+
properties: ["value", "currency", "email", "phone", "gclid"],
|
|
2614
|
+
serverSide: true
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
return events;
|
|
2619
|
+
}
|
|
2620
|
+
function getPlatformPostInstallSteps(config) {
|
|
2621
|
+
const steps = [];
|
|
2622
|
+
if (config.platformType === "mobile" || config.platformType === "both") {
|
|
2623
|
+
steps.push("Run `cd ios && pod install` to install native dependencies");
|
|
2624
|
+
if (config.enableDeepLinking) {
|
|
2625
|
+
steps.push("Configure URL schemes in Xcode/Android manifest for deep linking");
|
|
2626
|
+
steps.push("Set up Associated Domains (iOS) or App Links (Android)");
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
for (const platformId of config.adPlatforms) {
|
|
2630
|
+
const platform = AD_PLATFORMS.find((p2) => p2.id === platformId);
|
|
2631
|
+
if (!platform || platformId === "none") continue;
|
|
2632
|
+
steps.push(`Add ${platform.label} credentials to your environment variables`);
|
|
2633
|
+
if (platform.requiresServerSide) {
|
|
2634
|
+
steps.push(`Set up server-side endpoint for ${platform.label} conversions`);
|
|
2635
|
+
}
|
|
2636
|
+
if (config.platformType !== "web") {
|
|
2637
|
+
if (platformId === "meta") {
|
|
2638
|
+
steps.push("Add Meta App ID to Info.plist (iOS) and AndroidManifest.xml");
|
|
2639
|
+
}
|
|
2640
|
+
if (platformId === "tiktok") {
|
|
2641
|
+
steps.push("Add TikTok App ID to Info.plist (iOS) and AndroidManifest.xml");
|
|
2642
|
+
}
|
|
2643
|
+
if (platformId === "apple_search_ads") {
|
|
2644
|
+
steps.push("Enable AdServices.framework in Xcode");
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
return steps;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// src/generators/ai-context.ts
|
|
2652
|
+
function inferPropertyType(propName) {
|
|
2653
|
+
const lowerName = propName.toLowerCase();
|
|
2654
|
+
if (["value", "price", "amount", "total", "quantity", "count", "steps_completed"].some((n) => lowerName.includes(n))) {
|
|
2655
|
+
return "number";
|
|
2656
|
+
}
|
|
2657
|
+
if (["is_", "has_", "enable", "disable"].some((n) => lowerName.startsWith(n) || lowerName.includes(n))) {
|
|
2658
|
+
return "boolean";
|
|
2659
|
+
}
|
|
2660
|
+
if (lowerName === "currency") {
|
|
2661
|
+
return 'string; // ISO 4217 code (e.g., "USD", "EUR")';
|
|
2662
|
+
}
|
|
2663
|
+
if (lowerName === "email") {
|
|
2664
|
+
return "string; // User email for attribution matching";
|
|
2665
|
+
}
|
|
2666
|
+
if (lowerName === "phone") {
|
|
2667
|
+
return "string; // User phone for attribution matching";
|
|
2668
|
+
}
|
|
2669
|
+
if (lowerName.endsWith("_id") || lowerName === "id") {
|
|
2670
|
+
return "string";
|
|
2671
|
+
}
|
|
2672
|
+
return "string";
|
|
2673
|
+
}
|
|
2674
|
+
function generateAIContextDoc(params) {
|
|
2675
|
+
const {
|
|
2676
|
+
workspaceName,
|
|
2677
|
+
workspaceId,
|
|
2678
|
+
framework,
|
|
2679
|
+
platformType,
|
|
2680
|
+
adPlatforms,
|
|
2681
|
+
businessType,
|
|
2682
|
+
selectedEvents,
|
|
2683
|
+
sdks,
|
|
2684
|
+
enableServerSideConversions,
|
|
2685
|
+
enableContainer = true
|
|
2686
|
+
} = params;
|
|
2687
|
+
const eventsList = selectedEvents.map((e) => {
|
|
2688
|
+
const propsWithTypes = e.properties.map((p2) => {
|
|
2689
|
+
const type = inferPropertyType(p2);
|
|
2690
|
+
return ` ${p2}: ${type};`;
|
|
2691
|
+
}).join("\n");
|
|
2692
|
+
return `### \`${e.name}\`
|
|
2693
|
+
${e.description}
|
|
2694
|
+
|
|
2695
|
+
\`\`\`typescript
|
|
2696
|
+
datalyr.track('${e.name}', {
|
|
2697
|
+
${propsWithTypes}
|
|
2698
|
+
});
|
|
2699
|
+
\`\`\``;
|
|
2700
|
+
}).join("\n\n");
|
|
2701
|
+
const trackingExamples = selectedEvents.slice(0, 3).map((e) => {
|
|
2702
|
+
const props = e.properties.slice(0, 2).map((p2) => `${p2}: '...'`).join(", ");
|
|
2703
|
+
return `datalyr.track('${e.name}', { ${props} });`;
|
|
2704
|
+
}).join("\n");
|
|
2705
|
+
const serverSideExample = enableServerSideConversions ? `
|
|
2706
|
+
## Server-Side Tracking (CAPI)
|
|
2707
|
+
|
|
2708
|
+
For conversion events that need server-side tracking:
|
|
2709
|
+
|
|
2710
|
+
\`\`\`typescript
|
|
2711
|
+
// In your API route or server action
|
|
2712
|
+
import { datalyr } from '@/lib/datalyr.server';
|
|
2713
|
+
|
|
2714
|
+
await datalyr.track({
|
|
2715
|
+
event: 'purchase',
|
|
2716
|
+
userId: user.id,
|
|
2717
|
+
properties: {
|
|
2718
|
+
value: 99.00,
|
|
2719
|
+
currency: 'USD',
|
|
2720
|
+
email: user.email, // Required for ad platform matching
|
|
2721
|
+
phone: user.phone, // Optional, improves match rate
|
|
2722
|
+
},
|
|
2723
|
+
});
|
|
2724
|
+
\`\`\`
|
|
2725
|
+
` : "";
|
|
2726
|
+
const adPlatformsSection = adPlatforms.length > 0 ? `
|
|
2727
|
+
## Attribution Setup
|
|
2728
|
+
|
|
2729
|
+
Ad platforms configured: ${adPlatforms.join(", ")}
|
|
2730
|
+
|
|
2731
|
+
For proper attribution matching, include user data in conversion events:
|
|
2732
|
+
- \`email\`: User's email (hashed automatically)
|
|
2733
|
+
- \`phone\`: User's phone number (hashed automatically)
|
|
2734
|
+
${adPlatforms.includes("meta") ? "- `fbc`, `fbp`: Meta click/browser IDs (from cookies)" : ""}
|
|
2735
|
+
${adPlatforms.includes("google") ? "- `gclid`: Google click ID (from URL params)" : ""}
|
|
2736
|
+
${adPlatforms.includes("tiktok") ? "- `ttclid`: TikTok click ID (from URL params)" : ""}
|
|
2737
|
+
` : "";
|
|
2738
|
+
const containerSection = enableContainer ? `
|
|
2739
|
+
## Container Scripts
|
|
2740
|
+
|
|
2741
|
+
Container scripts are **enabled**. Third-party pixels (Meta Pixel, Google Tag, TikTok Pixel)
|
|
2742
|
+
are managed through the Datalyr dashboard, not in code.
|
|
2743
|
+
|
|
2744
|
+
Configure pixels at: https://app.datalyr.com/dashboard/${workspaceId}/settings/pixels
|
|
2745
|
+
|
|
2746
|
+
Benefits:
|
|
2747
|
+
- Add/remove pixels without code changes
|
|
2748
|
+
- Events tracked with \`datalyr.track()\` auto-fire to all configured pixels
|
|
2749
|
+
- Server-side fallback for ad blockers
|
|
2750
|
+
` : "";
|
|
2751
|
+
return `# Datalyr Analytics Setup
|
|
2752
|
+
|
|
2753
|
+
This file documents the Datalyr analytics configuration for AI coding assistants.
|
|
2754
|
+
|
|
2755
|
+
## Project Info
|
|
2756
|
+
|
|
2757
|
+
- **Workspace**: ${workspaceName}
|
|
2758
|
+
- **Workspace ID**: \`${workspaceId}\`
|
|
2759
|
+
- **Framework**: ${framework}
|
|
2760
|
+
- **Platform**: ${platformType}
|
|
2761
|
+
- **Business Type**: ${businessType}
|
|
2762
|
+
- **SDKs**: ${sdks.join(", ")}
|
|
2763
|
+
|
|
2764
|
+
## Quick Start
|
|
2765
|
+
|
|
2766
|
+
${framework === "ios" ? `\`\`\`swift
|
|
2767
|
+
import DatalyrSDK
|
|
2768
|
+
|
|
2769
|
+
// Track an event
|
|
2770
|
+
Datalyr.shared.track("event_name", properties: ["key": "value"])
|
|
2771
|
+
\`\`\`` : `\`\`\`typescript
|
|
2772
|
+
import datalyr from '@datalyr/web';
|
|
2773
|
+
|
|
2774
|
+
// Track an event
|
|
2775
|
+
${trackingExamples}
|
|
2776
|
+
\`\`\``}
|
|
2777
|
+
|
|
2778
|
+
## Events to Track
|
|
2779
|
+
|
|
2780
|
+
${eventsList}
|
|
2781
|
+
|
|
2782
|
+
## Usage Patterns
|
|
2783
|
+
|
|
2784
|
+
### Identify Users
|
|
2785
|
+
|
|
2786
|
+
\`\`\`typescript
|
|
2787
|
+
// After user signs in
|
|
2788
|
+
datalyr.identify(user.id, {
|
|
2789
|
+
email: user.email,
|
|
2790
|
+
name: user.name,
|
|
2791
|
+
plan: user.plan,
|
|
2792
|
+
});
|
|
2793
|
+
\`\`\`
|
|
2794
|
+
|
|
2795
|
+
### Track Events
|
|
2796
|
+
|
|
2797
|
+
\`\`\`typescript
|
|
2798
|
+
// Track any event with properties
|
|
2799
|
+
datalyr.track('event_name', {
|
|
2800
|
+
property: 'value',
|
|
2801
|
+
value: 100,
|
|
2802
|
+
});
|
|
2803
|
+
\`\`\`
|
|
2804
|
+
${serverSideExample}${adPlatformsSection}${containerSection}
|
|
2805
|
+
## Dashboard
|
|
2806
|
+
|
|
2807
|
+
View your analytics at:
|
|
2808
|
+
https://app.datalyr.com/dashboard/${workspaceId}/events
|
|
2809
|
+
|
|
2810
|
+
## Documentation
|
|
2811
|
+
|
|
2812
|
+
- SDK Docs: https://docs.datalyr.com/sdks/${framework}
|
|
2813
|
+
- API Reference: https://docs.datalyr.com/api
|
|
2814
|
+
`;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// src/agent/runner.ts
|
|
2818
|
+
var import_promises = require("fs/promises");
|
|
2819
|
+
var import_path5 = require("path");
|
|
2820
|
+
var import_fs4 = require("fs");
|
|
2821
|
+
var import_child_process = require("child_process");
|
|
2822
|
+
var import_util = require("util");
|
|
2823
|
+
var import_glob2 = require("glob");
|
|
2824
|
+
var execAsync = (0, import_util.promisify)(import_child_process.exec);
|
|
2825
|
+
var LLM_GATEWAY_URL = process.env.DATALYR_LLM_GATEWAY || "https://wizard.datalyr.com";
|
|
2826
|
+
async function runAgentWizard(_config, options = {}) {
|
|
2827
|
+
const cwd = options.cwd || process.cwd();
|
|
2828
|
+
p.intro(import_chalk4.default.cyan("Datalyr AI Wizard"));
|
|
2829
|
+
const useAI = await p.confirm({
|
|
2830
|
+
message: "This wizard uses AI to analyze your project and install Datalyr. Continue?",
|
|
2831
|
+
initialValue: true
|
|
2832
|
+
});
|
|
2833
|
+
if (p.isCancel(useAI) || !useAI) {
|
|
2834
|
+
p.cancel("Wizard cancelled");
|
|
2835
|
+
return { success: false, error: "User cancelled" };
|
|
2836
|
+
}
|
|
2837
|
+
let apiKey = options.apiKey;
|
|
2838
|
+
let workspace = null;
|
|
2839
|
+
let keyAttempts = 0;
|
|
2840
|
+
const maxAttempts = 3;
|
|
2841
|
+
while (!workspace && keyAttempts < maxAttempts) {
|
|
2842
|
+
keyAttempts++;
|
|
2843
|
+
if (!apiKey) {
|
|
2844
|
+
if (keyAttempts === 1) {
|
|
2845
|
+
p.note(
|
|
2846
|
+
`To get your API key:
|
|
2847
|
+
1. Go to ${import_chalk4.default.cyan("https://app.datalyr.com/settings/api")}
|
|
2848
|
+
2. Create a new API key (or copy existing)
|
|
2849
|
+
3. Paste it below`,
|
|
2850
|
+
"API Key Required"
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
const keyInput = await p.text({
|
|
2854
|
+
message: "Enter your Datalyr API key (starts with dk_):",
|
|
2855
|
+
placeholder: "dk_live_...",
|
|
2856
|
+
validate: (value) => {
|
|
2857
|
+
if (!value) return "API key is required";
|
|
2858
|
+
if (!value.startsWith("dk_")) return "API key must start with dk_";
|
|
2859
|
+
if (value.length < 20) return "API key seems too short";
|
|
2860
|
+
return void 0;
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
2863
|
+
if (p.isCancel(keyInput)) {
|
|
2864
|
+
p.cancel("Wizard cancelled");
|
|
2865
|
+
return { success: false, error: "User cancelled" };
|
|
2866
|
+
}
|
|
2867
|
+
apiKey = keyInput;
|
|
2868
|
+
}
|
|
2869
|
+
const validateSpinner = p.spinner();
|
|
2870
|
+
validateSpinner.start("Validating API key...");
|
|
2871
|
+
try {
|
|
2872
|
+
const defaultWorkspace = await validateApiKey(apiKey);
|
|
2873
|
+
const allWorkspaces = await fetchWorkspaces(apiKey);
|
|
2874
|
+
if (allWorkspaces.length > 1) {
|
|
2875
|
+
validateSpinner.stop("API key validated");
|
|
2876
|
+
const workspaceChoice = await p.select({
|
|
2877
|
+
message: "Select a workspace to configure:",
|
|
2878
|
+
options: allWorkspaces.map((w) => ({
|
|
2879
|
+
value: w.id,
|
|
2880
|
+
label: w.name,
|
|
2881
|
+
hint: w.domain || void 0
|
|
2882
|
+
}))
|
|
2883
|
+
});
|
|
2884
|
+
if (p.isCancel(workspaceChoice)) {
|
|
2885
|
+
p.cancel("Wizard cancelled");
|
|
2886
|
+
return { success: false, error: "User cancelled" };
|
|
2887
|
+
}
|
|
2888
|
+
const selectedWorkspace = allWorkspaces.find((w) => w.id === workspaceChoice);
|
|
2889
|
+
if (selectedWorkspace) {
|
|
2890
|
+
workspace = {
|
|
2891
|
+
id: selectedWorkspace.id,
|
|
2892
|
+
name: selectedWorkspace.name,
|
|
2893
|
+
timezone: null,
|
|
2894
|
+
domain: selectedWorkspace.domain
|
|
2895
|
+
};
|
|
2896
|
+
} else {
|
|
2897
|
+
workspace = defaultWorkspace;
|
|
2898
|
+
}
|
|
2899
|
+
p.log.info(`Selected workspace: ${import_chalk4.default.cyan(workspace.name)}`);
|
|
2900
|
+
} else {
|
|
2901
|
+
workspace = defaultWorkspace;
|
|
2902
|
+
validateSpinner.stop(`Workspace: ${import_chalk4.default.cyan(workspace.name)}`);
|
|
2903
|
+
}
|
|
2904
|
+
} catch (error) {
|
|
2905
|
+
validateSpinner.stop(import_chalk4.default.red("Invalid API key"));
|
|
2906
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to validate API key";
|
|
2907
|
+
p.log.error(errorMessage);
|
|
2908
|
+
apiKey = void 0;
|
|
2909
|
+
if (keyAttempts < maxAttempts) {
|
|
2910
|
+
const action = await p.select({
|
|
2911
|
+
message: "What would you like to do?",
|
|
2912
|
+
options: [
|
|
2913
|
+
{ value: "retry", label: "Try a different API key" },
|
|
2914
|
+
{ value: "signup", label: "Create a free account", hint: "Opens browser" },
|
|
2915
|
+
{ value: "exit", label: "Exit wizard" }
|
|
2916
|
+
]
|
|
2917
|
+
});
|
|
2918
|
+
if (p.isCancel(action) || action === "exit") {
|
|
2919
|
+
p.cancel("Wizard cancelled");
|
|
2920
|
+
return { success: false, error: "User cancelled" };
|
|
2921
|
+
}
|
|
2922
|
+
if (action === "signup") {
|
|
2923
|
+
const signupUrl = "https://app.datalyr.com/signup?ref=wizard";
|
|
2924
|
+
p.log.info(`Opening ${import_chalk4.default.cyan(signupUrl)} in your browser...`);
|
|
2925
|
+
try {
|
|
2926
|
+
const { exec: exec2 } = await import("child_process");
|
|
2927
|
+
const { promisify: promisify2 } = await import("util");
|
|
2928
|
+
const execAsync2 = promisify2(exec2);
|
|
2929
|
+
const platform = process.platform;
|
|
2930
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
2931
|
+
await execAsync2(`${cmd} "${signupUrl}"`);
|
|
2932
|
+
p.log.success("Browser opened! Create your account and come back with your API key.");
|
|
2933
|
+
await p.text({
|
|
2934
|
+
message: "Press Enter when you have your API key ready..."
|
|
2935
|
+
});
|
|
2936
|
+
} catch {
|
|
2937
|
+
p.log.warn(`Please visit: ${import_chalk4.default.cyan(signupUrl)}`);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
if (!workspace || !apiKey) {
|
|
2944
|
+
p.log.error(`Failed to validate API key after ${maxAttempts} attempts.`);
|
|
2945
|
+
p.log.info(`Get help at ${import_chalk4.default.cyan("https://docs.datalyr.com/getting-started")}`);
|
|
2946
|
+
return { success: false, error: "API key validation failed" };
|
|
2947
|
+
}
|
|
2948
|
+
const validatedApiKey = apiKey;
|
|
2949
|
+
const detectSpinner = p.spinner();
|
|
2950
|
+
detectSpinner.start("Analyzing your project...");
|
|
2951
|
+
let detection = await detectFramework(cwd);
|
|
2952
|
+
let framework = detection.framework;
|
|
2953
|
+
if (framework === "unknown" && !options.framework) {
|
|
2954
|
+
detectSpinner.stop("Could not auto-detect framework");
|
|
2955
|
+
const frameworkChoice = await p.select({
|
|
2956
|
+
message: "Select your framework:",
|
|
2957
|
+
options: [
|
|
2958
|
+
{ value: "nextjs", label: "Next.js" },
|
|
2959
|
+
{ value: "react", label: "React" },
|
|
2960
|
+
{ value: "react-vite", label: "React (Vite)" },
|
|
2961
|
+
{ value: "svelte", label: "Svelte" },
|
|
2962
|
+
{ value: "sveltekit", label: "SvelteKit" },
|
|
2963
|
+
{ value: "react-native", label: "React Native" },
|
|
2964
|
+
{ value: "expo", label: "Expo" },
|
|
2965
|
+
{ value: "ios", label: "iOS (Swift)" },
|
|
2966
|
+
{ value: "node", label: "Node.js" }
|
|
2967
|
+
]
|
|
2968
|
+
});
|
|
2969
|
+
if (p.isCancel(frameworkChoice)) {
|
|
2970
|
+
p.cancel("Wizard cancelled");
|
|
2971
|
+
return { success: false, error: "User cancelled" };
|
|
2972
|
+
}
|
|
2973
|
+
framework = frameworkChoice;
|
|
2974
|
+
} else if (options.framework) {
|
|
2975
|
+
framework = options.framework;
|
|
2976
|
+
}
|
|
2977
|
+
detectSpinner.stop(`Detected: ${import_chalk4.default.cyan(getFrameworkDisplayName(framework))}`);
|
|
2978
|
+
if (detection.framework !== framework) {
|
|
2979
|
+
detection = { ...detection, framework, sdks: getSdksForFramework(framework) };
|
|
2980
|
+
}
|
|
2981
|
+
const isMobileFramework = ["react-native", "expo", "ios"].includes(framework);
|
|
2982
|
+
const isWebFramework = ["nextjs", "react", "react-vite", "svelte", "sveltekit"].includes(framework);
|
|
2983
|
+
let platformType = "web";
|
|
2984
|
+
if (isMobileFramework) {
|
|
2985
|
+
platformType = "mobile";
|
|
2986
|
+
} else if (!isMobileFramework && !isWebFramework) {
|
|
2987
|
+
const platformChoice = await p.select({
|
|
2988
|
+
message: "What platforms are you targeting?",
|
|
2989
|
+
options: PLATFORM_TYPES.map((type) => ({
|
|
2990
|
+
value: type.id,
|
|
2991
|
+
label: type.label,
|
|
2992
|
+
hint: type.hint
|
|
2993
|
+
}))
|
|
2994
|
+
});
|
|
2995
|
+
if (p.isCancel(platformChoice)) {
|
|
2996
|
+
p.cancel("Wizard cancelled");
|
|
2997
|
+
return { success: false, error: "User cancelled" };
|
|
2998
|
+
}
|
|
2999
|
+
platformType = platformChoice;
|
|
3000
|
+
}
|
|
3001
|
+
const runningAds = await p.confirm({
|
|
3002
|
+
message: "Are you running paid ads (Meta, Google, TikTok)?",
|
|
3003
|
+
initialValue: false
|
|
3004
|
+
});
|
|
3005
|
+
let adPlatforms = [];
|
|
3006
|
+
let platformConfig = {
|
|
3007
|
+
platformType,
|
|
3008
|
+
adPlatforms: [],
|
|
3009
|
+
enableDeepLinking: false,
|
|
3010
|
+
enableServerSideConversions: false
|
|
3011
|
+
};
|
|
3012
|
+
if (!p.isCancel(runningAds) && runningAds) {
|
|
3013
|
+
const adPlatformChoices = await p.multiselect({
|
|
3014
|
+
message: "Select your ad platforms:",
|
|
3015
|
+
options: AD_PLATFORMS.filter((p2) => p2.id !== "none").map((platform) => ({
|
|
3016
|
+
value: platform.id,
|
|
3017
|
+
label: platform.label,
|
|
3018
|
+
hint: platform.description
|
|
3019
|
+
})),
|
|
3020
|
+
required: false
|
|
3021
|
+
});
|
|
3022
|
+
if (!p.isCancel(adPlatformChoices)) {
|
|
3023
|
+
adPlatforms = adPlatformChoices;
|
|
3024
|
+
if (adPlatforms.length > 0) {
|
|
3025
|
+
const serverSide = await p.confirm({
|
|
3026
|
+
message: "Enable server-side conversion tracking (CAPI)?",
|
|
3027
|
+
initialValue: true
|
|
3028
|
+
});
|
|
3029
|
+
platformConfig.enableServerSideConversions = !p.isCancel(serverSide) && serverSide;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
if (platformType === "mobile" || platformType === "both") {
|
|
3034
|
+
const deepLinking = await p.confirm({
|
|
3035
|
+
message: "Set up deep linking / deferred deep links?",
|
|
3036
|
+
initialValue: adPlatforms.length > 0
|
|
3037
|
+
});
|
|
3038
|
+
platformConfig.enableDeepLinking = !p.isCancel(deepLinking) && deepLinking;
|
|
3039
|
+
}
|
|
3040
|
+
platformConfig.adPlatforms = adPlatforms;
|
|
3041
|
+
let enableContainer = options.enableContainer;
|
|
3042
|
+
if (enableContainer === void 0 && platformType !== "mobile") {
|
|
3043
|
+
const containerChoice = await p.confirm({
|
|
3044
|
+
message: "Manage third-party pixels through Datalyr dashboard?",
|
|
3045
|
+
initialValue: adPlatforms.length > 0
|
|
3046
|
+
// Default yes if using ad platforms
|
|
3047
|
+
});
|
|
3048
|
+
if (!p.isCancel(containerChoice)) {
|
|
3049
|
+
enableContainer = containerChoice;
|
|
3050
|
+
if (containerChoice) {
|
|
3051
|
+
p.note(
|
|
3052
|
+
`Container scripts let you:
|
|
3053
|
+
${import_chalk4.default.green("\u2022")} Add/remove Meta, Google, TikTok pixels without code
|
|
3054
|
+
${import_chalk4.default.green("\u2022")} Auto-fire datalyr.track() events to all pixels
|
|
3055
|
+
${import_chalk4.default.green("\u2022")} Inject custom scripts (Hotjar, Intercom, etc.)`,
|
|
3056
|
+
"Container Scripts"
|
|
3057
|
+
);
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
const containerEnabled = enableContainer !== false;
|
|
3062
|
+
const businessTypeChoice = await p.select({
|
|
3063
|
+
message: "What type of app are you building?",
|
|
3064
|
+
options: BUSINESS_TYPES.map((type) => ({
|
|
3065
|
+
value: type.id,
|
|
3066
|
+
label: type.label,
|
|
3067
|
+
hint: type.hint
|
|
3068
|
+
}))
|
|
3069
|
+
});
|
|
3070
|
+
if (p.isCancel(businessTypeChoice)) {
|
|
3071
|
+
p.cancel("Wizard cancelled");
|
|
3072
|
+
return { success: false, error: "User cancelled" };
|
|
3073
|
+
}
|
|
3074
|
+
const selectedBusinessType = businessTypeChoice;
|
|
3075
|
+
const suggestedEvents = getEventSuggestions(selectedBusinessType);
|
|
3076
|
+
const attributionEvents = getAttributionEvents(platformConfig);
|
|
3077
|
+
const autoTrackedEvents = platformType === "mobile" || platformType === "both" ? ["page_view / screen_view", "session_start", "app_install (mobile)", "attribution data"] : ["page_view", "session_start", "referrer", "UTM parameters"];
|
|
3078
|
+
p.note(
|
|
3079
|
+
`${import_chalk4.default.bold("Auto-tracked (no code needed):")}
|
|
3080
|
+
` + autoTrackedEvents.map((e) => ` ${import_chalk4.default.dim("\u2022")} ${e}`).join("\n") + `
|
|
3081
|
+
|
|
3082
|
+
${import_chalk4.default.bold("High-value events")} are the key actions that matter for your business:
|
|
3083
|
+
${import_chalk4.default.dim("\u2022")} Conversions (signups, purchases, leads)
|
|
3084
|
+
${import_chalk4.default.dim("\u2022")} Engagement (feature usage, key interactions)
|
|
3085
|
+
${import_chalk4.default.dim("\u2022")} Revenue (transactions, subscriptions)`,
|
|
3086
|
+
"Event Tracking"
|
|
3087
|
+
);
|
|
3088
|
+
const allEvents = [...suggestedEvents, ...attributionEvents.map((e) => ({
|
|
3089
|
+
name: e.name,
|
|
3090
|
+
description: e.description,
|
|
3091
|
+
properties: e.properties,
|
|
3092
|
+
priority: "high"
|
|
3093
|
+
}))];
|
|
3094
|
+
const uniqueEvents = allEvents.filter(
|
|
3095
|
+
(event, index, self) => index === self.findIndex((e) => e.name === event.name)
|
|
3096
|
+
);
|
|
3097
|
+
const eventOptions = buildEventSelectOptions(uniqueEvents, true);
|
|
3098
|
+
const businessTypeLabel = BUSINESS_TYPES.find((b) => b.id === selectedBusinessType)?.label || selectedBusinessType;
|
|
3099
|
+
p.note(
|
|
3100
|
+
`Based on your ${import_chalk4.default.cyan(businessTypeLabel)} app, we suggest these events:
|
|
3101
|
+
|
|
3102
|
+
` + uniqueEvents.filter((e) => e.priority === "high").map((e) => ` ${import_chalk4.default.green("\u2022")} ${import_chalk4.default.bold(e.name)}: ${formatEventDescription(e)}`).join("\n"),
|
|
3103
|
+
"Suggested Events"
|
|
3104
|
+
);
|
|
3105
|
+
const selectedEventNames = await p.multiselect({
|
|
3106
|
+
message: "Select events to track:",
|
|
3107
|
+
options: eventOptions,
|
|
3108
|
+
initialValues: ["__all_recommended__"],
|
|
3109
|
+
required: false
|
|
3110
|
+
});
|
|
3111
|
+
let selectedEvents = p.isCancel(selectedEventNames) ? uniqueEvents.filter((e) => e.priority === "high") : resolveSelectedEvents(selectedEventNames, uniqueEvents);
|
|
3112
|
+
const addCustom = await p.confirm({
|
|
3113
|
+
message: "Want to add any custom events specific to your app?",
|
|
3114
|
+
initialValue: false
|
|
3115
|
+
});
|
|
3116
|
+
if (!p.isCancel(addCustom) && addCustom) {
|
|
3117
|
+
const customEventInput = await p.text({
|
|
3118
|
+
message: "Enter custom event names (comma-separated):",
|
|
3119
|
+
placeholder: "checkout_completed, feature_clicked, ..."
|
|
3120
|
+
});
|
|
3121
|
+
if (!p.isCancel(customEventInput) && customEventInput) {
|
|
3122
|
+
const customEvents = customEventInput.split(",").map((e) => e.trim()).filter((e) => e.length > 0).map((name) => ({
|
|
3123
|
+
name,
|
|
3124
|
+
description: "Custom event",
|
|
3125
|
+
properties: ["value", "context"],
|
|
3126
|
+
priority: "high"
|
|
3127
|
+
}));
|
|
3128
|
+
selectedEvents = [...selectedEvents, ...customEvents];
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
if (selectedEvents.length > 0) {
|
|
3132
|
+
const eventList = selectedEvents.slice(0, 6).map((e) => ` ${import_chalk4.default.green("\u2022")} ${import_chalk4.default.bold(e.name)}`).join("\n");
|
|
3133
|
+
const moreCount = selectedEvents.length > 6 ? ` (+${selectedEvents.length - 6} more)` : "";
|
|
3134
|
+
p.note(
|
|
3135
|
+
`Events you'll track:
|
|
3136
|
+
|
|
3137
|
+
${eventList}${moreCount}`,
|
|
3138
|
+
"Your Events"
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
const platformSdks = getSdksForPlatform(platformType, adPlatforms);
|
|
3142
|
+
const allSdks = [.../* @__PURE__ */ new Set([...detection.sdks, ...platformSdks])];
|
|
3143
|
+
const platformSteps = getPlatformPostInstallSteps(platformConfig);
|
|
3144
|
+
p.note(
|
|
3145
|
+
`The wizard will:
|
|
3146
|
+
${import_chalk4.default.green("\u2022")} Install ${import_chalk4.default.cyan(allSdks.join(", "))}
|
|
3147
|
+
${import_chalk4.default.green("\u2022")} Create initialization code
|
|
3148
|
+
${import_chalk4.default.green("\u2022")} Configure environment variables
|
|
3149
|
+
${import_chalk4.default.green("\u2022")} Set up workspace: ${import_chalk4.default.cyan(workspace.name)}
|
|
3150
|
+
${adPlatforms.length > 0 ? `${import_chalk4.default.green("\u2022")} Configure attribution for: ${import_chalk4.default.cyan(adPlatforms.join(", "))}` : ""}
|
|
3151
|
+
${containerEnabled && platformType !== "mobile" ? `${import_chalk4.default.green("\u2022")} Enable container scripts for pixel management` : ""}`,
|
|
3152
|
+
"Installation Plan"
|
|
3153
|
+
);
|
|
3154
|
+
const proceed = await p.confirm({
|
|
3155
|
+
message: "Proceed with installation?",
|
|
3156
|
+
initialValue: true
|
|
3157
|
+
});
|
|
3158
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
3159
|
+
p.cancel("Wizard cancelled");
|
|
3160
|
+
return { success: false, error: "User cancelled" };
|
|
3161
|
+
}
|
|
3162
|
+
const agentSpinner = p.spinner();
|
|
3163
|
+
agentSpinner.start("AI agent is working...");
|
|
3164
|
+
try {
|
|
3165
|
+
const result = await executeAgent({
|
|
3166
|
+
framework,
|
|
3167
|
+
apiKey: validatedApiKey,
|
|
3168
|
+
cwd,
|
|
3169
|
+
docs: getFrameworkDocs(framework, validatedApiKey),
|
|
3170
|
+
debug: options.debug,
|
|
3171
|
+
enableContainer: containerEnabled,
|
|
3172
|
+
workspaceId: workspace.id
|
|
3173
|
+
});
|
|
3174
|
+
if (result.success) {
|
|
3175
|
+
agentSpinner.stop(import_chalk4.default.green("Installation complete!"));
|
|
3176
|
+
if (!options.skipVerification) {
|
|
3177
|
+
const verifyResult = await verifyInstallation(validatedApiKey, workspace.id);
|
|
3178
|
+
if (verifyResult.success) {
|
|
3179
|
+
p.log.success("SDK verified and ready to track events!");
|
|
3180
|
+
} else {
|
|
3181
|
+
p.log.warn("Could not verify SDK installation. This may be normal if events haven't been sent yet.");
|
|
3182
|
+
p.log.info(`Check your dashboard at: ${import_chalk4.default.cyan(`https://app.datalyr.com/dashboard/${workspace.id}/events`)}`);
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
try {
|
|
3186
|
+
const aiContextDoc = generateAIContextDoc({
|
|
3187
|
+
workspaceName: workspace.name,
|
|
3188
|
+
workspaceId: workspace.id,
|
|
3189
|
+
framework,
|
|
3190
|
+
platformType,
|
|
3191
|
+
adPlatforms,
|
|
3192
|
+
businessType: selectedBusinessType,
|
|
3193
|
+
selectedEvents,
|
|
3194
|
+
sdks: allSdks,
|
|
3195
|
+
enableServerSideConversions: platformConfig.enableServerSideConversions,
|
|
3196
|
+
enableContainer: containerEnabled
|
|
3197
|
+
});
|
|
3198
|
+
await (0, import_promises.writeFile)((0, import_path5.join)(cwd, ".datalyr.md"), aiContextDoc);
|
|
3199
|
+
p.log.success("Created .datalyr.md for AI coding assistants");
|
|
3200
|
+
} catch {
|
|
3201
|
+
}
|
|
3202
|
+
const eventExamples = selectedEvents.slice(0, 3).map(
|
|
3203
|
+
(e) => `datalyr.track('${e.name}', { ${e.properties.slice(0, 2).map((p2) => `${p2}: '...'`).join(", ")} })`
|
|
3204
|
+
).join("\n ");
|
|
3205
|
+
const stepsText = platformSteps.length > 0 ? `
|
|
3206
|
+
|
|
3207
|
+
${import_chalk4.default.bold("Next steps:")}
|
|
3208
|
+
${platformSteps.map((s) => ` ${import_chalk4.default.yellow("\u2192")} ${s}`).join("\n")}` : "";
|
|
3209
|
+
p.note(
|
|
3210
|
+
`Your workspace "${workspace.name}" is ready!
|
|
3211
|
+
|
|
3212
|
+
${import_chalk4.default.bold("Start tracking events:")}
|
|
3213
|
+
${eventExamples || "datalyr.track('event_name', { property: 'value' })"}
|
|
3214
|
+
|
|
3215
|
+
${import_chalk4.default.bold("View your data:")}
|
|
3216
|
+
${import_chalk4.default.cyan(`https://app.datalyr.com/dashboard/${workspace.id}/events`)}${stepsText}`,
|
|
3217
|
+
"Success!"
|
|
3218
|
+
);
|
|
3219
|
+
p.outro(import_chalk4.default.green("Datalyr is ready to use!"));
|
|
3220
|
+
} else {
|
|
3221
|
+
agentSpinner.stop(import_chalk4.default.red("Installation failed"));
|
|
3222
|
+
p.log.error(result.error || "Unknown error");
|
|
3223
|
+
}
|
|
3224
|
+
return result;
|
|
3225
|
+
} catch (error) {
|
|
3226
|
+
agentSpinner.stop(import_chalk4.default.red("Agent error"));
|
|
3227
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
3228
|
+
p.log.error(errorMessage);
|
|
3229
|
+
return { success: false, error: errorMessage };
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
async function executeAgent(params) {
|
|
3233
|
+
const { framework, apiKey, cwd, docs, debug, enableContainer = true, workspaceId } = params;
|
|
3234
|
+
const initialMessage = buildIntegrationPrompt(framework, apiKey, docs, enableContainer, workspaceId);
|
|
3235
|
+
const messages = [
|
|
3236
|
+
{ role: "user", content: initialMessage }
|
|
3237
|
+
];
|
|
3238
|
+
const filesModified = [];
|
|
3239
|
+
const maxIterations = 20;
|
|
3240
|
+
let iterations = 0;
|
|
3241
|
+
while (iterations < maxIterations) {
|
|
3242
|
+
iterations++;
|
|
3243
|
+
if (debug) {
|
|
3244
|
+
console.log(`[DEBUG] Agent iteration ${iterations}`);
|
|
3245
|
+
}
|
|
3246
|
+
try {
|
|
3247
|
+
const controller = new AbortController();
|
|
3248
|
+
const timeoutId = setTimeout(() => controller.abort(), 6e4);
|
|
3249
|
+
const response = await fetch(`${LLM_GATEWAY_URL}/agent`, {
|
|
3250
|
+
method: "POST",
|
|
3251
|
+
headers: {
|
|
3252
|
+
"Content-Type": "application/json"
|
|
3253
|
+
},
|
|
3254
|
+
body: JSON.stringify({
|
|
3255
|
+
messages,
|
|
3256
|
+
system: buildSystemPrompt()
|
|
3257
|
+
}),
|
|
3258
|
+
signal: controller.signal
|
|
3259
|
+
});
|
|
3260
|
+
clearTimeout(timeoutId);
|
|
3261
|
+
if (!response.ok) {
|
|
3262
|
+
const error = await response.text();
|
|
3263
|
+
return { success: false, error: `Gateway error: ${error}` };
|
|
3264
|
+
}
|
|
3265
|
+
const claudeResponse = await response.json();
|
|
3266
|
+
if (debug) {
|
|
3267
|
+
console.log("[DEBUG] Response:", JSON.stringify(claudeResponse, null, 2));
|
|
3268
|
+
}
|
|
3269
|
+
messages.push({ role: "assistant", content: claudeResponse.content });
|
|
3270
|
+
if (claudeResponse.stop_reason === "end_turn") {
|
|
3271
|
+
const taskComplete = claudeResponse.content.find(
|
|
3272
|
+
(block) => block.type === "tool_use" && block.name === "task_complete"
|
|
3273
|
+
);
|
|
3274
|
+
if (taskComplete) {
|
|
3275
|
+
const input2 = taskComplete.input;
|
|
3276
|
+
return {
|
|
3277
|
+
success: input2.success,
|
|
3278
|
+
message: input2.summary,
|
|
3279
|
+
filesModified: input2.files_modified || filesModified
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
if (filesModified.length > 0) {
|
|
3283
|
+
return {
|
|
3284
|
+
success: false,
|
|
3285
|
+
message: "Agent stopped unexpectedly. Files were modified but installation may be incomplete.",
|
|
3286
|
+
error: "Agent did not call task_complete. Please verify the installation manually.",
|
|
3287
|
+
filesModified
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
return {
|
|
3291
|
+
success: false,
|
|
3292
|
+
error: "Agent stopped without completing the installation task."
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3295
|
+
if (claudeResponse.stop_reason === "tool_use") {
|
|
3296
|
+
const toolUseBlocks = claudeResponse.content.filter(
|
|
3297
|
+
(block) => block.type === "tool_use"
|
|
3298
|
+
);
|
|
3299
|
+
const toolResults = [];
|
|
3300
|
+
for (const toolUse of toolUseBlocks) {
|
|
3301
|
+
if (toolUse.name === "task_complete") {
|
|
3302
|
+
const input2 = toolUse.input;
|
|
3303
|
+
return {
|
|
3304
|
+
success: input2.success,
|
|
3305
|
+
message: input2.summary,
|
|
3306
|
+
filesModified: input2.files_modified || filesModified
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
const result = await executeTool(toolUse, cwd, debug);
|
|
3310
|
+
if (toolUse.name === "write_file" && !result.is_error) {
|
|
3311
|
+
const path6 = toolUse.input.path;
|
|
3312
|
+
if (!filesModified.includes(path6)) {
|
|
3313
|
+
filesModified.push(path6);
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
toolResults.push({
|
|
3317
|
+
type: "tool_result",
|
|
3318
|
+
tool_use_id: toolUse.id,
|
|
3319
|
+
content: result.content,
|
|
3320
|
+
is_error: result.is_error
|
|
3321
|
+
});
|
|
3322
|
+
}
|
|
3323
|
+
messages.push({ role: "user", content: toolResults });
|
|
3324
|
+
}
|
|
3325
|
+
} catch (error) {
|
|
3326
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
3327
|
+
if (debug) {
|
|
3328
|
+
console.error("[DEBUG] Agent error:", error);
|
|
3329
|
+
}
|
|
3330
|
+
return { success: false, error: `Agent error: ${errorMessage}` };
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
return { success: false, error: "Agent exceeded maximum iterations" };
|
|
3334
|
+
}
|
|
3335
|
+
function isPathSafe(basePath, targetPath) {
|
|
3336
|
+
const resolvedBase = (0, import_path5.resolve)(basePath);
|
|
3337
|
+
const resolvedTarget = (0, import_path5.resolve)(basePath, targetPath);
|
|
3338
|
+
return resolvedTarget.startsWith(resolvedBase + "/") || resolvedTarget === resolvedBase;
|
|
3339
|
+
}
|
|
3340
|
+
async function executeTool(toolUse, cwd, debug) {
|
|
3341
|
+
const { name, input: input2 } = toolUse;
|
|
3342
|
+
if (debug) {
|
|
3343
|
+
console.log(`[DEBUG] Executing tool: ${name}`, input2);
|
|
3344
|
+
}
|
|
3345
|
+
try {
|
|
3346
|
+
switch (name) {
|
|
3347
|
+
case "read_file": {
|
|
3348
|
+
if (typeof input2.path !== "string" || !input2.path.trim()) {
|
|
3349
|
+
return { content: "Error: path must be a non-empty string", is_error: true };
|
|
3350
|
+
}
|
|
3351
|
+
const path6 = input2.path.trim();
|
|
3352
|
+
if (!isPathSafe(cwd, path6)) {
|
|
3353
|
+
return { content: "Error: path traversal detected - access denied", is_error: true };
|
|
3354
|
+
}
|
|
3355
|
+
const fullPath = (0, import_path5.join)(cwd, path6);
|
|
3356
|
+
const content = await (0, import_promises.readFile)(fullPath, "utf-8");
|
|
3357
|
+
return { content };
|
|
3358
|
+
}
|
|
3359
|
+
case "write_file": {
|
|
3360
|
+
if (typeof input2.path !== "string" || !input2.path.trim()) {
|
|
3361
|
+
return { content: "Error: path must be a non-empty string", is_error: true };
|
|
3362
|
+
}
|
|
3363
|
+
if (typeof input2.content !== "string") {
|
|
3364
|
+
return { content: "Error: content must be a string", is_error: true };
|
|
3365
|
+
}
|
|
3366
|
+
const path6 = input2.path.trim();
|
|
3367
|
+
const content = input2.content;
|
|
3368
|
+
if (!isPathSafe(cwd, path6)) {
|
|
3369
|
+
return { content: "Error: path traversal detected - access denied", is_error: true };
|
|
3370
|
+
}
|
|
3371
|
+
const fullPath = (0, import_path5.join)(cwd, path6);
|
|
3372
|
+
const dir = (0, import_path5.dirname)(fullPath);
|
|
3373
|
+
if (!(0, import_fs4.existsSync)(dir)) {
|
|
3374
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
3375
|
+
}
|
|
3376
|
+
await (0, import_promises.writeFile)(fullPath, content, "utf-8");
|
|
3377
|
+
return { content: `Successfully wrote ${path6}` };
|
|
3378
|
+
}
|
|
3379
|
+
case "run_command": {
|
|
3380
|
+
if (typeof input2.command !== "string" || !input2.command.trim()) {
|
|
3381
|
+
return { content: "Error: command must be a non-empty string", is_error: true };
|
|
3382
|
+
}
|
|
3383
|
+
const command = input2.command.trim();
|
|
3384
|
+
const validation = validateBashCommand(command);
|
|
3385
|
+
if (!validation.allowed) {
|
|
3386
|
+
return {
|
|
3387
|
+
content: `Command blocked: ${validation.reason}`,
|
|
3388
|
+
is_error: true
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3391
|
+
const { stdout, stderr } = await execAsync(command, { cwd, timeout: 12e4 });
|
|
3392
|
+
return { content: stdout || stderr || "Command completed successfully" };
|
|
3393
|
+
}
|
|
3394
|
+
case "list_files": {
|
|
3395
|
+
if (typeof input2.path !== "string") {
|
|
3396
|
+
return { content: "Error: path must be a string", is_error: true };
|
|
3397
|
+
}
|
|
3398
|
+
const path6 = input2.path.trim() || ".";
|
|
3399
|
+
const pattern = typeof input2.pattern === "string" ? input2.pattern : "*";
|
|
3400
|
+
if (!isPathSafe(cwd, path6)) {
|
|
3401
|
+
return { content: "Error: path traversal detected - access denied", is_error: true };
|
|
3402
|
+
}
|
|
3403
|
+
const fullPath = (0, import_path5.join)(cwd, path6);
|
|
3404
|
+
const files = await (0, import_glob2.glob)(pattern, {
|
|
3405
|
+
cwd: fullPath,
|
|
3406
|
+
nodir: false,
|
|
3407
|
+
maxDepth: 3
|
|
3408
|
+
});
|
|
3409
|
+
return { content: files.slice(0, 100).join("\n") };
|
|
3410
|
+
}
|
|
3411
|
+
default:
|
|
3412
|
+
return { content: `Unknown tool: ${name}`, is_error: true };
|
|
3413
|
+
}
|
|
3414
|
+
} catch (error) {
|
|
3415
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
3416
|
+
return { content: `Error: ${errorMessage}`, is_error: true };
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
function buildIntegrationPrompt(framework, apiKey, docs, enableContainer, workspaceId) {
|
|
3420
|
+
const containerConfig = enableContainer ? " enableContainer: true, // Load third-party pixels from Datalyr dashboard" : " enableContainer: false, // Container scripts disabled";
|
|
3421
|
+
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";
|
|
3422
|
+
return `Install Datalyr analytics into this ${getFrameworkDisplayName(framework)} project.
|
|
3423
|
+
|
|
3424
|
+
## Workspace ID
|
|
3425
|
+
${workspaceId}
|
|
3426
|
+
|
|
3427
|
+
## API Key (for server-side only)
|
|
3428
|
+
${apiKey}
|
|
3429
|
+
|
|
3430
|
+
## Integration Documentation
|
|
3431
|
+
${docs}
|
|
3432
|
+
|
|
3433
|
+
## SDK Configuration
|
|
3434
|
+
When initializing the SDK, use these options:
|
|
3435
|
+
\`\`\`typescript
|
|
3436
|
+
datalyr.init({
|
|
3437
|
+
workspaceId: process.env.${envVarName},
|
|
3438
|
+
${containerConfig}
|
|
3439
|
+
});
|
|
3440
|
+
\`\`\`
|
|
3441
|
+
|
|
3442
|
+
## Environment Variable
|
|
3443
|
+
Add to .env.local (or .env):
|
|
3444
|
+
${envVarName}=${workspaceId}
|
|
3445
|
+
|
|
3446
|
+
## Your Task
|
|
3447
|
+
|
|
3448
|
+
1. Read package.json to understand the project
|
|
3449
|
+
2. Install the SDK: run the appropriate install command
|
|
3450
|
+
3. Create initialization code following the documentation
|
|
3451
|
+
4. Add the environment variable to .env.local or .env
|
|
3452
|
+
5. Call task_complete when done
|
|
3453
|
+
|
|
3454
|
+
Start by reading package.json.`;
|
|
3455
|
+
}
|
|
3456
|
+
async function validateApiKey(apiKey) {
|
|
3457
|
+
const controller = new AbortController();
|
|
3458
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
3459
|
+
const response = await fetch(`${LLM_GATEWAY_URL}/validate-key`, {
|
|
3460
|
+
method: "POST",
|
|
3461
|
+
headers: {
|
|
3462
|
+
"Content-Type": "application/json"
|
|
3463
|
+
},
|
|
3464
|
+
body: JSON.stringify({ apiKey }),
|
|
3465
|
+
signal: controller.signal
|
|
3466
|
+
});
|
|
3467
|
+
clearTimeout(timeoutId);
|
|
3468
|
+
if (!response.ok) {
|
|
3469
|
+
const error = await response.json();
|
|
3470
|
+
throw new Error(error.error || "Failed to validate API key");
|
|
3471
|
+
}
|
|
3472
|
+
const result = await response.json();
|
|
3473
|
+
if (!result.success || !result.workspace) {
|
|
3474
|
+
throw new Error(result.error || "Invalid API key");
|
|
3475
|
+
}
|
|
3476
|
+
return result.workspace;
|
|
3477
|
+
}
|
|
3478
|
+
async function fetchWorkspaces(apiKey) {
|
|
3479
|
+
const controller = new AbortController();
|
|
3480
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
3481
|
+
const response = await fetch(`${LLM_GATEWAY_URL}/workspaces`, {
|
|
3482
|
+
method: "POST",
|
|
3483
|
+
headers: {
|
|
3484
|
+
"Content-Type": "application/json"
|
|
3485
|
+
},
|
|
3486
|
+
body: JSON.stringify({ apiKey }),
|
|
3487
|
+
signal: controller.signal
|
|
3488
|
+
});
|
|
3489
|
+
clearTimeout(timeoutId);
|
|
3490
|
+
if (!response.ok) {
|
|
3491
|
+
return [];
|
|
3492
|
+
}
|
|
3493
|
+
const result = await response.json();
|
|
3494
|
+
if (!result.success || !result.workspaces) {
|
|
3495
|
+
return [];
|
|
3496
|
+
}
|
|
3497
|
+
return result.workspaces;
|
|
3498
|
+
}
|
|
3499
|
+
async function verifyInstallation(apiKey, workspaceId) {
|
|
3500
|
+
try {
|
|
3501
|
+
const controller = new AbortController();
|
|
3502
|
+
const timeoutId = setTimeout(() => controller.abort(), 15e3);
|
|
3503
|
+
const response = await fetch(`${LLM_GATEWAY_URL}/verify`, {
|
|
3504
|
+
method: "POST",
|
|
3505
|
+
headers: {
|
|
3506
|
+
"Content-Type": "application/json"
|
|
3507
|
+
},
|
|
3508
|
+
body: JSON.stringify({ apiKey, workspaceId }),
|
|
3509
|
+
signal: controller.signal
|
|
3510
|
+
});
|
|
3511
|
+
clearTimeout(timeoutId);
|
|
3512
|
+
if (!response.ok) {
|
|
3513
|
+
return { success: false };
|
|
3514
|
+
}
|
|
3515
|
+
const result = await response.json();
|
|
3516
|
+
return result;
|
|
3517
|
+
} catch {
|
|
3518
|
+
return { success: false };
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
|
|
1152
3522
|
// src/index.ts
|
|
1153
3523
|
async function runWizard(options = {}) {
|
|
1154
3524
|
const cwd = options.cwd || process.cwd();
|
|
@@ -1166,9 +3536,9 @@ async function runWizard(options = {}) {
|
|
|
1166
3536
|
if (!options.json) {
|
|
1167
3537
|
logger.step(2, 5, "Analyzing your project...");
|
|
1168
3538
|
}
|
|
1169
|
-
const
|
|
3539
|
+
const spinner2 = options.json ? null : startSpinner("Scanning project structure...");
|
|
1170
3540
|
let detection = await detectFramework(cwd);
|
|
1171
|
-
if (
|
|
3541
|
+
if (spinner2) {
|
|
1172
3542
|
succeedSpinner(`Detected ${getFrameworkDisplayName(detection.framework)}`);
|
|
1173
3543
|
}
|
|
1174
3544
|
if (detection.framework === "unknown" || options.framework) {
|
|
@@ -1247,7 +3617,7 @@ async function runWizard(options = {}) {
|
|
|
1247
3617
|
}
|
|
1248
3618
|
}
|
|
1249
3619
|
for (const file of plan.files) {
|
|
1250
|
-
const filePath =
|
|
3620
|
+
const filePath = import_path6.default.join(cwd, file.path);
|
|
1251
3621
|
const exists = await fileExists(filePath);
|
|
1252
3622
|
if (exists && file.action === "create") {
|
|
1253
3623
|
logger.warn(`Skipping ${file.path} (already exists)`);
|
|
@@ -1307,6 +3677,7 @@ async function findEntryPoints(cwd, framework) {
|
|
|
1307
3677
|
0 && (module.exports = {
|
|
1308
3678
|
detectFramework,
|
|
1309
3679
|
generateInstallationPlan,
|
|
3680
|
+
runAgentWizard,
|
|
1310
3681
|
runWizard
|
|
1311
3682
|
});
|
|
1312
3683
|
//# sourceMappingURL=index.js.map
|