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