@easyfunnel/mcp 0.1.10 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +450 -38
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -241,6 +241,58 @@ var frameworkConfigs = {
|
|
|
241
241
|
layoutPaths: ["src/routes/+layout.svelte"]
|
|
242
242
|
}
|
|
243
243
|
};
|
|
244
|
+
var helperPaths = {
|
|
245
|
+
nextjs: {
|
|
246
|
+
helperFile: "lib/easyfunnel.ts",
|
|
247
|
+
bridgeFile: "components/ef-bridge.tsx",
|
|
248
|
+
helperImport: "@/lib/easyfunnel",
|
|
249
|
+
bridgeImport: "@/components/ef-bridge"
|
|
250
|
+
},
|
|
251
|
+
vite: {
|
|
252
|
+
helperFile: "src/lib/easyfunnel.ts",
|
|
253
|
+
bridgeFile: "src/components/ef-bridge.tsx",
|
|
254
|
+
helperImport: "@/lib/easyfunnel",
|
|
255
|
+
bridgeImport: "@/components/ef-bridge"
|
|
256
|
+
},
|
|
257
|
+
cra: {
|
|
258
|
+
helperFile: "src/lib/easyfunnel.ts",
|
|
259
|
+
bridgeFile: "src/components/ef-bridge.tsx",
|
|
260
|
+
helperImport: "@/lib/easyfunnel",
|
|
261
|
+
bridgeImport: "@/components/ef-bridge"
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
var HELPER_FILE_CONTENT = `import type { Tracker } from '@easyfunnel/sdk'
|
|
265
|
+
|
|
266
|
+
let tracker: Tracker | null = null
|
|
267
|
+
|
|
268
|
+
export function setTracker(t: Tracker | null) { tracker = t }
|
|
269
|
+
export function getTracker(): Tracker | null { return tracker }
|
|
270
|
+
|
|
271
|
+
export function track(event: string, properties?: Record<string, any>) {
|
|
272
|
+
tracker?.track(event, properties)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function identify(userId: string) {
|
|
276
|
+
tracker?.identify(userId)
|
|
277
|
+
}
|
|
278
|
+
`;
|
|
279
|
+
function getBridgeFileContent(helperImport) {
|
|
280
|
+
return `'use client'
|
|
281
|
+
|
|
282
|
+
import { useEffect } from 'react'
|
|
283
|
+
import { useEasyFunnel } from '@easyfunnel/react'
|
|
284
|
+
import { setTracker } from '${helperImport}'
|
|
285
|
+
|
|
286
|
+
export function EfBridge() {
|
|
287
|
+
const tracker = useEasyFunnel()
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
setTracker(tracker)
|
|
290
|
+
return () => setTracker(null)
|
|
291
|
+
}, [tracker])
|
|
292
|
+
return null
|
|
293
|
+
}
|
|
294
|
+
`;
|
|
295
|
+
}
|
|
244
296
|
function detectPackageManager(projectRoot) {
|
|
245
297
|
if ((0, import_fs.existsSync)((0, import_path.join)(projectRoot, "bun.lockb"))) return "bun";
|
|
246
298
|
if ((0, import_fs.existsSync)((0, import_path.join)(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
@@ -398,6 +450,62 @@ Next: After adding the script, I'll verify everything works with a test event.`
|
|
|
398
450
|
if (!providerWrapped) {
|
|
399
451
|
steps.push(`[skip] Could not find layout file to add provider. Searched: ${config.layoutPaths.join(", ")}`);
|
|
400
452
|
}
|
|
453
|
+
const paths = framework !== "html" && framework !== "sveltekit" ? helperPaths[framework] : null;
|
|
454
|
+
if (paths) {
|
|
455
|
+
const helperFullPath = (0, import_path.join)(project_root, paths.helperFile);
|
|
456
|
+
const helperDir = (0, import_path.dirname)(helperFullPath);
|
|
457
|
+
if ((0, import_fs.existsSync)(helperFullPath)) {
|
|
458
|
+
const existing = (0, import_fs.readFileSync)(helperFullPath, "utf-8");
|
|
459
|
+
if (existing.includes("setTracker")) {
|
|
460
|
+
steps.push(`[skip] ${paths.helperFile} already exists`);
|
|
461
|
+
} else {
|
|
462
|
+
steps.push(`[skip] ${paths.helperFile} exists but may need manual update \u2014 expected setTracker export`);
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
if (!(0, import_fs.existsSync)(helperDir)) {
|
|
466
|
+
(0, import_fs.mkdirSync)(helperDir, { recursive: true });
|
|
467
|
+
}
|
|
468
|
+
(0, import_fs.writeFileSync)(helperFullPath, HELPER_FILE_CONTENT);
|
|
469
|
+
steps.push(`[done] Generated ${paths.helperFile} (tracking helper for non-React files)`);
|
|
470
|
+
filesModified.push({ file: paths.helperFile, action: "Generated tracking helper" });
|
|
471
|
+
}
|
|
472
|
+
const bridgeFullPath = (0, import_path.join)(project_root, paths.bridgeFile);
|
|
473
|
+
const bridgeDir = (0, import_path.dirname)(bridgeFullPath);
|
|
474
|
+
if ((0, import_fs.existsSync)(bridgeFullPath)) {
|
|
475
|
+
steps.push(`[skip] ${paths.bridgeFile} already exists`);
|
|
476
|
+
} else {
|
|
477
|
+
if (!(0, import_fs.existsSync)(bridgeDir)) {
|
|
478
|
+
(0, import_fs.mkdirSync)(bridgeDir, { recursive: true });
|
|
479
|
+
}
|
|
480
|
+
(0, import_fs.writeFileSync)(bridgeFullPath, getBridgeFileContent(paths.helperImport));
|
|
481
|
+
steps.push(`[done] Generated ${paths.bridgeFile} (syncs React context to helper)`);
|
|
482
|
+
filesModified.push({ file: paths.bridgeFile, action: "Generated bridge component" });
|
|
483
|
+
}
|
|
484
|
+
if (providerWrapped) {
|
|
485
|
+
for (const relPath of config.layoutPaths) {
|
|
486
|
+
const fullPath = (0, import_path.join)(project_root, relPath);
|
|
487
|
+
if (!(0, import_fs.existsSync)(fullPath)) continue;
|
|
488
|
+
let content = (0, import_fs.readFileSync)(fullPath, "utf-8");
|
|
489
|
+
if (!content.includes("EasyFunnelProvider")) continue;
|
|
490
|
+
if (content.includes("EfBridge")) {
|
|
491
|
+
steps.push(`[skip] EfBridge already present in ${relPath}`);
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
const bridgeImportLine = `import { EfBridge } from '${paths.bridgeImport}'
|
|
495
|
+
`;
|
|
496
|
+
content = bridgeImportLine + content;
|
|
497
|
+
content = content.replace(
|
|
498
|
+
/(<EasyFunnelProvider[^>]*>)([\s\S]*?)(\{children\})/,
|
|
499
|
+
`$1$2<EfBridge />
|
|
500
|
+
$3`
|
|
501
|
+
);
|
|
502
|
+
(0, import_fs.writeFileSync)(fullPath, content);
|
|
503
|
+
steps.push(`[done] Added <EfBridge /> to ${relPath}`);
|
|
504
|
+
filesModified.push({ file: relPath, action: "Added bridge component" });
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
401
509
|
let output = `SDK SETUP COMPLETE
|
|
402
510
|
|
|
403
511
|
`;
|
|
@@ -428,6 +536,23 @@ The API key is hardcoded in your layout file \u2014 no env var configuration nee
|
|
|
428
536
|
`;
|
|
429
537
|
output += `If you prefer, you can override it via ${config.envVarName} in ${config.envFile}.
|
|
430
538
|
`;
|
|
539
|
+
if (paths) {
|
|
540
|
+
output += `
|
|
541
|
+
Tracking helper: ${paths.helperFile}
|
|
542
|
+
`;
|
|
543
|
+
output += ` - React components: use useTrack() from @easyfunnel/react
|
|
544
|
+
`;
|
|
545
|
+
output += ` - Non-React files: import { track } from '${paths.helperImport}'
|
|
546
|
+
`;
|
|
547
|
+
} else if (framework === "html") {
|
|
548
|
+
output += `
|
|
549
|
+
Tracking: use window.EasyFunnel.track() \u2014 the SDK global is available after page load.
|
|
550
|
+
`;
|
|
551
|
+
} else if (framework === "sveltekit") {
|
|
552
|
+
output += `
|
|
553
|
+
Note: Tracking outside Svelte components requires a manual singleton pattern.
|
|
554
|
+
`;
|
|
555
|
+
}
|
|
431
556
|
output += `
|
|
432
557
|
Next: I'll verify everything works with a test event.`;
|
|
433
558
|
return {
|
|
@@ -565,9 +690,22 @@ async function scanForActions(args) {
|
|
|
565
690
|
|
|
566
691
|
// src/tools/instrument-code.ts
|
|
567
692
|
var import_fs3 = require("fs");
|
|
693
|
+
|
|
694
|
+
// src/lib/react-detect.ts
|
|
695
|
+
function isReactComponent(filePath, content) {
|
|
696
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
697
|
+
if (ext !== "tsx" && ext !== "jsx") return false;
|
|
698
|
+
return content.includes("'use client'") || content.includes('"use client"') || /import\s+React/m.test(content) || /from\s+['"]react['"]/m.test(content) || /return\s*\(/m.test(content);
|
|
699
|
+
}
|
|
700
|
+
function isReactAware(filePath, content) {
|
|
701
|
+
if (isReactComponent(filePath, content)) return true;
|
|
702
|
+
return /from\s+['"]react['"]/m.test(content) || /from\s+['"]@easyfunnel\/react['"]/m.test(content);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/tools/instrument-code.ts
|
|
568
706
|
var instrumentCodeDefinition = {
|
|
569
707
|
name: "instrument_code",
|
|
570
|
-
description: "Add easyfunnel tracking to a specific interactive element
|
|
708
|
+
description: "Add easyfunnel tracking to a specific interactive element. attribute method returns a diff preview. track_call method returns context-aware suggestions (useTrack for React components, helper import for other files). Does NOT auto-save.",
|
|
571
709
|
inputSchema: {
|
|
572
710
|
type: "object",
|
|
573
711
|
properties: {
|
|
@@ -586,7 +724,7 @@ var instrumentCodeDefinition = {
|
|
|
586
724
|
method: {
|
|
587
725
|
type: "string",
|
|
588
726
|
enum: ["attribute", "track_call"],
|
|
589
|
-
description: "attribute: add data-ef-track attribute. track_call: add
|
|
727
|
+
description: "attribute: add data-ef-track attribute. track_call: add track() call \u2014 automatically suggests useTrack() for React components or import from lib/easyfunnel for other files."
|
|
590
728
|
}
|
|
591
729
|
},
|
|
592
730
|
required: ["file", "line", "event_name", "method"]
|
|
@@ -607,8 +745,6 @@ async function instrumentCode(args) {
|
|
|
607
745
|
]
|
|
608
746
|
};
|
|
609
747
|
}
|
|
610
|
-
let newLine;
|
|
611
|
-
let changeDescription;
|
|
612
748
|
if (method === "attribute") {
|
|
613
749
|
if (targetLine.includes("data-ef-track")) {
|
|
614
750
|
return {
|
|
@@ -620,54 +756,98 @@ async function instrumentCode(args) {
|
|
|
620
756
|
]
|
|
621
757
|
};
|
|
622
758
|
}
|
|
623
|
-
newLine = targetLine.replace(
|
|
759
|
+
let newLine = targetLine.replace(
|
|
624
760
|
/(<\w+)(\s)/,
|
|
625
761
|
`$1 data-ef-track="${event_name}"$2`
|
|
626
762
|
);
|
|
627
763
|
if (newLine === targetLine) {
|
|
628
764
|
newLine = targetLine.replace(/>/, ` data-ef-track="${event_name}">`);
|
|
629
765
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
766
|
+
const changeDescription2 = `Added data-ef-track="${event_name}" attribute`;
|
|
767
|
+
const diff = `--- ${file}
|
|
768
|
+
+++ ${file}
|
|
769
|
+
@@ -${line},1 +${line},1 @@
|
|
770
|
+
-${targetLine}
|
|
771
|
+
+${newLine}`;
|
|
772
|
+
return {
|
|
773
|
+
content: [
|
|
774
|
+
{
|
|
775
|
+
type: "text",
|
|
776
|
+
text: `${changeDescription2}
|
|
777
|
+
|
|
778
|
+
Diff preview:
|
|
779
|
+
\`\`\`diff
|
|
780
|
+
${diff}
|
|
781
|
+
\`\`\`
|
|
782
|
+
|
|
783
|
+
Apply this change to ${file} at line ${line}.`
|
|
784
|
+
}
|
|
785
|
+
]
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
const isReact = isReactComponent(file, content);
|
|
789
|
+
const hasHandler = targetLine.includes("onClick") || targetLine.includes("onSubmit");
|
|
790
|
+
const handlerHint = hasHandler ? `
|
|
791
|
+
|
|
792
|
+
The target line contains an event handler \u2014 add the track call at the start of that handler function.` : "";
|
|
793
|
+
if (!isReact && isReactAware(file, content)) {
|
|
794
|
+
const changeDescription2 = `Add track('${event_name}') call (React-aware file detected \u2014 could be a hook or utility).
|
|
795
|
+
|
|
796
|
+
Option A (if this is a React hook/component):
|
|
797
|
+
1. import { useTrack } from '@easyfunnel/react'
|
|
798
|
+
2. const track = useTrack()
|
|
799
|
+
3. track('${event_name}')
|
|
800
|
+
|
|
801
|
+
Option B (if this is a utility/helper):
|
|
802
|
+
1. import { track } from '@/lib/easyfunnel'
|
|
803
|
+
2. track('${event_name}')` + handlerHint;
|
|
804
|
+
return {
|
|
805
|
+
content: [
|
|
806
|
+
{
|
|
807
|
+
type: "text",
|
|
808
|
+
text: `Instrumentation suggestion for ${file}:${line}
|
|
809
|
+
|
|
810
|
+
${changeDescription2}
|
|
811
|
+
|
|
812
|
+
Please apply the appropriate option using your code editing capabilities.`
|
|
813
|
+
}
|
|
814
|
+
]
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
if (isReact) {
|
|
818
|
+
const changeDescription2 = `Add track('${event_name}') call (React component detected).
|
|
635
819
|
|
|
636
820
|
1. Add import: import { useTrack } from '@easyfunnel/react'
|
|
637
|
-
2. Add hook:
|
|
638
|
-
3. Add
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
821
|
+
2. Add hook: const track = useTrack()
|
|
822
|
+
3. Add call: track('${event_name}') at the start of the handler` + handlerHint;
|
|
823
|
+
return {
|
|
824
|
+
content: [
|
|
825
|
+
{
|
|
826
|
+
type: "text",
|
|
827
|
+
text: `Instrumentation suggestion for ${file}:${line}
|
|
644
828
|
|
|
645
|
-
${
|
|
829
|
+
${changeDescription2}
|
|
646
830
|
|
|
647
831
|
This requires modifying the component. Please apply these changes using your code editing capabilities.`
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
}
|
|
652
|
-
changeDescription = `Suggested: add track('${event_name}') call in the handler near line ${line}`;
|
|
832
|
+
}
|
|
833
|
+
]
|
|
834
|
+
};
|
|
653
835
|
}
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
836
|
+
const changeDescription = `Add track('${event_name}') call (non-React file detected).
|
|
837
|
+
|
|
838
|
+
1. Add import: import { track } from '@/lib/easyfunnel'
|
|
839
|
+
2. Add call: track('${event_name}') at the relevant point` + handlerHint + `
|
|
840
|
+
|
|
841
|
+
Note: lib/easyfunnel.ts is generated by setup_sdk. If it doesn't exist, run setup_sdk first.`;
|
|
659
842
|
return {
|
|
660
843
|
content: [
|
|
661
844
|
{
|
|
662
845
|
type: "text",
|
|
663
|
-
text:
|
|
846
|
+
text: `Instrumentation suggestion for ${file}:${line}
|
|
664
847
|
|
|
665
|
-
|
|
666
|
-
\`\`\`diff
|
|
667
|
-
${diff}
|
|
668
|
-
\`\`\`
|
|
848
|
+
${changeDescription}
|
|
669
849
|
|
|
670
|
-
|
|
850
|
+
Please apply these changes using your code editing capabilities.`
|
|
671
851
|
}
|
|
672
852
|
]
|
|
673
853
|
};
|
|
@@ -832,8 +1012,8 @@ var queryEventsDefinition = {
|
|
|
832
1012
|
project_id: { type: "string", description: "Project ID" },
|
|
833
1013
|
query_type: {
|
|
834
1014
|
type: "string",
|
|
835
|
-
enum: ["count", "recent", "breakdown", "section_engagement", "traffic_sources"],
|
|
836
|
-
description:
|
|
1015
|
+
enum: ["count", "recent", "breakdown", "section_engagement", "traffic_sources", "engagement"],
|
|
1016
|
+
description: 'Type of query. Use "engagement" to get scroll depth and engaged time metrics.'
|
|
837
1017
|
},
|
|
838
1018
|
event_name: {
|
|
839
1019
|
type: "string",
|
|
@@ -871,7 +1051,9 @@ async function queryEvents(client2, args) {
|
|
|
871
1051
|
`;
|
|
872
1052
|
output += ` Unique sessions: ${data.unique_sessions}
|
|
873
1053
|
`;
|
|
874
|
-
output += ` Unique
|
|
1054
|
+
output += ` Unique visitors: ${data.unique_visitors}
|
|
1055
|
+
`;
|
|
1056
|
+
output += ` Identified users: ${data.identified_users}
|
|
875
1057
|
`;
|
|
876
1058
|
} else if (args.query_type === "recent") {
|
|
877
1059
|
output = `Recent events:
|
|
@@ -881,8 +1063,43 @@ async function queryEvents(client2, args) {
|
|
|
881
1063
|
output += ` [${event.created_at}] ${event.event_name}`;
|
|
882
1064
|
if (event.properties?.url) output += ` \u2014 ${event.properties.url}`;
|
|
883
1065
|
output += "\n";
|
|
1066
|
+
if (event.properties) {
|
|
1067
|
+
const props = { ...event.properties };
|
|
1068
|
+
delete props.url;
|
|
1069
|
+
const keys = Object.keys(props);
|
|
1070
|
+
if (keys.length > 0) {
|
|
1071
|
+
output += ` properties: ${JSON.stringify(props)}
|
|
1072
|
+
`;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
884
1075
|
}
|
|
885
1076
|
if (!data.events?.length) output += " No events found.\n";
|
|
1077
|
+
} else if (args.query_type === "engagement") {
|
|
1078
|
+
output = `Engagement metrics (${args.time_range || "7d"}):
|
|
1079
|
+
|
|
1080
|
+
`;
|
|
1081
|
+
output += ` Avg scroll depth: ${data.avg_scroll_depth}%
|
|
1082
|
+
`;
|
|
1083
|
+
output += ` Avg engaged time: ${data.avg_engaged_time}s
|
|
1084
|
+
`;
|
|
1085
|
+
output += ` Total engagement events: ${data.total_events}
|
|
1086
|
+
|
|
1087
|
+
`;
|
|
1088
|
+
output += ` Scroll depth distribution:
|
|
1089
|
+
`;
|
|
1090
|
+
for (const bucket of data.scroll_depth_distribution || []) {
|
|
1091
|
+
output += ` ${bucket.bucket}: ${bucket.count} events
|
|
1092
|
+
`;
|
|
1093
|
+
}
|
|
1094
|
+
if (data.top_pages?.length) {
|
|
1095
|
+
output += `
|
|
1096
|
+
Top pages by scroll depth:
|
|
1097
|
+
`;
|
|
1098
|
+
for (const page of data.top_pages) {
|
|
1099
|
+
output += ` ${page.url}: ${page.avg_scroll_depth}% avg, ${page.count} events
|
|
1100
|
+
`;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
886
1103
|
} else if (args.query_type === "section_engagement") {
|
|
887
1104
|
output = `Section engagement (${args.time_range || "7d"}):
|
|
888
1105
|
|
|
@@ -1681,6 +1898,69 @@ function findHardcodedKey(projectRoot) {
|
|
|
1681
1898
|
}
|
|
1682
1899
|
return null;
|
|
1683
1900
|
}
|
|
1901
|
+
function scanAntiPatterns(projectRoot, isReactFramework) {
|
|
1902
|
+
const warnings = [];
|
|
1903
|
+
const files = walkDir2(projectRoot, [".ts", ".tsx", ".js", ".jsx"], [], 300);
|
|
1904
|
+
const patterns = [
|
|
1905
|
+
{
|
|
1906
|
+
id: "window_ef",
|
|
1907
|
+
regex: /window\.ef(?![\w.])/,
|
|
1908
|
+
condition: () => true,
|
|
1909
|
+
message: "window.ef doesn't exist. Import { track } from '@/lib/easyfunnel' instead."
|
|
1910
|
+
},
|
|
1911
|
+
{
|
|
1912
|
+
id: "global_track",
|
|
1913
|
+
regex: /window\.EasyFunnel\.track/,
|
|
1914
|
+
condition: () => isReactFramework,
|
|
1915
|
+
message: "Direct global access is fragile in React apps. Import { track } from '@/lib/easyfunnel' instead."
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
id: "hook_outside_component",
|
|
1919
|
+
regex: /useTrack\(\)/,
|
|
1920
|
+
condition: (filePath, content) => !isReactComponent(filePath, content),
|
|
1921
|
+
message: "useTrack() only works inside React components. Use import { track } from '@/lib/easyfunnel' for non-component files."
|
|
1922
|
+
}
|
|
1923
|
+
];
|
|
1924
|
+
for (const file of files) {
|
|
1925
|
+
const content = readFileSafe(file);
|
|
1926
|
+
if (!content) continue;
|
|
1927
|
+
const relPath = file.replace(projectRoot + "/", "");
|
|
1928
|
+
const lines = content.split("\n");
|
|
1929
|
+
for (const pattern of patterns) {
|
|
1930
|
+
if (!pattern.condition(file, content)) continue;
|
|
1931
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1932
|
+
if (pattern.regex.test(lines[i])) {
|
|
1933
|
+
warnings.push({
|
|
1934
|
+
file: relPath,
|
|
1935
|
+
line: i + 1,
|
|
1936
|
+
id: pattern.id,
|
|
1937
|
+
message: pattern.message
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
if (isReactFramework) {
|
|
1944
|
+
const helperExists = (0, import_fs6.existsSync)((0, import_path4.join)(projectRoot, "lib/easyfunnel.ts")) || (0, import_fs6.existsSync)((0, import_path4.join)(projectRoot, "src/lib/easyfunnel.ts"));
|
|
1945
|
+
if (!helperExists) {
|
|
1946
|
+
const hasTrackingOutsideReact = files.some((f) => {
|
|
1947
|
+
const content = readFileSafe(f);
|
|
1948
|
+
if (!content) return false;
|
|
1949
|
+
if (isReactComponent(f, content)) return false;
|
|
1950
|
+
return content.includes(".track(") || content.includes("window.ef") || content.includes("window.EasyFunnel");
|
|
1951
|
+
});
|
|
1952
|
+
if (hasTrackingOutsideReact) {
|
|
1953
|
+
warnings.push({
|
|
1954
|
+
file: "",
|
|
1955
|
+
line: 0,
|
|
1956
|
+
id: "missing_helper",
|
|
1957
|
+
message: "No tracking helper found. Run setup_sdk to generate lib/easyfunnel.ts."
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
return warnings;
|
|
1963
|
+
}
|
|
1684
1964
|
async function validateSetup(client2, args) {
|
|
1685
1965
|
const { project_root, project_id } = args;
|
|
1686
1966
|
const checks = [];
|
|
@@ -1780,6 +2060,13 @@ async function validateSetup(client2, args) {
|
|
|
1780
2060
|
});
|
|
1781
2061
|
}
|
|
1782
2062
|
}
|
|
2063
|
+
const isReactFramework = (() => {
|
|
2064
|
+
const pkgPath = (0, import_path4.join)(project_root, "package.json");
|
|
2065
|
+
if (!(0, import_fs6.existsSync)(pkgPath)) return false;
|
|
2066
|
+
const pkg = (0, import_fs6.readFileSync)(pkgPath, "utf-8");
|
|
2067
|
+
return pkg.includes('"react"') || pkg.includes("'react'");
|
|
2068
|
+
})();
|
|
2069
|
+
const antiPatterns = scanAntiPatterns(project_root, isReactFramework);
|
|
1783
2070
|
const allPassed = checks.every((c) => c.passed);
|
|
1784
2071
|
const failedChecks = checks.filter((c) => !c.passed);
|
|
1785
2072
|
let output = "";
|
|
@@ -1829,6 +2116,22 @@ Next: Let me suggest conversion funnels based on what I found in your codebase.`
|
|
|
1829
2116
|
}
|
|
1830
2117
|
}
|
|
1831
2118
|
}
|
|
2119
|
+
if (antiPatterns.length > 0) {
|
|
2120
|
+
output += `
|
|
2121
|
+
Recommendations:
|
|
2122
|
+
`;
|
|
2123
|
+
for (const w of antiPatterns) {
|
|
2124
|
+
if (w.file) {
|
|
2125
|
+
output += ` [WARN] ${w.file}:${w.line} \u2014 ${w.message}
|
|
2126
|
+
`;
|
|
2127
|
+
} else {
|
|
2128
|
+
output += ` [WARN] ${w.message}
|
|
2129
|
+
`;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
output += `
|
|
2133
|
+
`;
|
|
2134
|
+
}
|
|
1832
2135
|
return {
|
|
1833
2136
|
content: [{ type: "text", text: output }]
|
|
1834
2137
|
};
|
|
@@ -2202,6 +2505,112 @@ ${msg.content}
|
|
|
2202
2505
|
};
|
|
2203
2506
|
}
|
|
2204
2507
|
|
|
2508
|
+
// src/tools/get-best-practices.ts
|
|
2509
|
+
var getBestPracticesDefinition = {
|
|
2510
|
+
name: "get_best_practices",
|
|
2511
|
+
description: "Get framework-specific tracking best practices. Call this BEFORE writing any tracking code to avoid common integration mistakes.",
|
|
2512
|
+
inputSchema: {
|
|
2513
|
+
type: "object",
|
|
2514
|
+
properties: {
|
|
2515
|
+
framework: {
|
|
2516
|
+
type: "string",
|
|
2517
|
+
enum: ["nextjs", "vite", "cra", "sveltekit", "html"],
|
|
2518
|
+
description: "The framework. If omitted, returns general guidance for all frameworks."
|
|
2519
|
+
}
|
|
2520
|
+
},
|
|
2521
|
+
required: []
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
var TRACKING_PATTERNS = `TRACKING PATTERNS \u2014 How to track events
|
|
2525
|
+
|
|
2526
|
+
React components:
|
|
2527
|
+
import { useTrack } from '@easyfunnel/react'
|
|
2528
|
+
const track = useTrack()
|
|
2529
|
+
track('event_name', { key: 'value' })
|
|
2530
|
+
|
|
2531
|
+
Non-React files (utils, API handlers, state stores, callbacks):
|
|
2532
|
+
import { track } from '@/lib/easyfunnel'
|
|
2533
|
+
track('event_name', { key: 'value' })
|
|
2534
|
+
|
|
2535
|
+
HTML / script-tag sites:
|
|
2536
|
+
window.EasyFunnel.track('event_name', { key: 'value' })
|
|
2537
|
+
|
|
2538
|
+
Declarative (zero code \u2014 just add an attribute):
|
|
2539
|
+
<button data-ef-track="click_signup">Sign Up</button>`;
|
|
2540
|
+
var COMMON_MISTAKES = `COMMON MISTAKES \u2014 What NOT to do
|
|
2541
|
+
|
|
2542
|
+
1. window.ef does NOT exist.
|
|
2543
|
+
The SDK never sets window.ef. If you see code polling for it, remove it.
|
|
2544
|
+
Use: import { track } from '@/lib/easyfunnel'
|
|
2545
|
+
|
|
2546
|
+
2. useTrack() outside React components silently drops events.
|
|
2547
|
+
Hooks require React context. In plain .ts files, use the helper module.
|
|
2548
|
+
Use: import { track } from '@/lib/easyfunnel'
|
|
2549
|
+
|
|
2550
|
+
3. window.EasyFunnel.track() in React apps is fragile.
|
|
2551
|
+
It bypasses the React lifecycle and may fire before SDK initializes.
|
|
2552
|
+
Use: useTrack() in components, or import { track } from '@/lib/easyfunnel'
|
|
2553
|
+
|
|
2554
|
+
4. Custom global polling/bridging code is unnecessary.
|
|
2555
|
+
setup_sdk generates lib/easyfunnel.ts and a bridge component automatically.
|
|
2556
|
+
Do NOT write your own window.* bridge \u2014 use the generated helper.`;
|
|
2557
|
+
var CHECKLISTS = {
|
|
2558
|
+
nextjs: `SETUP CHECKLIST \u2014 Next.js
|
|
2559
|
+
|
|
2560
|
+
[1] EasyFunnelProvider wraps {children} in app/layout.tsx
|
|
2561
|
+
[2] EfBridge component rendered inside the provider (syncs context to helper)
|
|
2562
|
+
[3] lib/easyfunnel.ts exists (generated by setup_sdk)
|
|
2563
|
+
[4] Components using useTrack() have 'use client' directive
|
|
2564
|
+
[5] API key set via NEXT_PUBLIC_EASYFUNNEL_KEY in .env.local`,
|
|
2565
|
+
vite: `SETUP CHECKLIST \u2014 Vite
|
|
2566
|
+
|
|
2567
|
+
[1] EasyFunnelProvider wraps the app in src/App.tsx or src/main.tsx
|
|
2568
|
+
[2] EfBridge component rendered inside the provider
|
|
2569
|
+
[3] src/lib/easyfunnel.ts exists (generated by setup_sdk)
|
|
2570
|
+
[4] API key set via VITE_EASYFUNNEL_KEY in .env`,
|
|
2571
|
+
cra: `SETUP CHECKLIST \u2014 Create React App
|
|
2572
|
+
|
|
2573
|
+
[1] EasyFunnelProvider wraps the app in src/App.tsx
|
|
2574
|
+
[2] EfBridge component rendered inside the provider
|
|
2575
|
+
[3] src/lib/easyfunnel.ts exists (generated by setup_sdk)
|
|
2576
|
+
[4] API key set via REACT_APP_EASYFUNNEL_KEY in .env`,
|
|
2577
|
+
sveltekit: `SETUP CHECKLIST \u2014 SvelteKit
|
|
2578
|
+
|
|
2579
|
+
[1] SDK loaded via <script> tag in +layout.svelte (React provider does NOT work in Svelte)
|
|
2580
|
+
[2] Tracking outside components requires a manual singleton pattern
|
|
2581
|
+
[3] API key set via PUBLIC_EASYFUNNEL_KEY in .env`,
|
|
2582
|
+
html: `SETUP CHECKLIST \u2014 HTML / Script Tag
|
|
2583
|
+
|
|
2584
|
+
[1] <script defer data-api-key="ef_..." src="https://easyfunnel.co/sdk.js"></script> in <head>
|
|
2585
|
+
[2] Track via window.EasyFunnel.track() after page load
|
|
2586
|
+
[3] No helper module needed \u2014 window.EasyFunnel IS the global`
|
|
2587
|
+
};
|
|
2588
|
+
var GENERAL_CHECKLIST = `SETUP CHECKLIST \u2014 General
|
|
2589
|
+
|
|
2590
|
+
React frameworks (Next.js, Vite, CRA):
|
|
2591
|
+
[1] EasyFunnelProvider wraps the app
|
|
2592
|
+
[2] EfBridge component rendered inside the provider
|
|
2593
|
+
[3] lib/easyfunnel.ts helper exists for non-component tracking
|
|
2594
|
+
[4] Next.js: 'use client' on components using hooks
|
|
2595
|
+
|
|
2596
|
+
HTML / Script Tag:
|
|
2597
|
+
[1] SDK script tag in <head>
|
|
2598
|
+
[2] Track via window.EasyFunnel.track()
|
|
2599
|
+
|
|
2600
|
+
Pass framework parameter for a framework-specific checklist.`;
|
|
2601
|
+
async function getBestPractices(args) {
|
|
2602
|
+
const { framework } = args;
|
|
2603
|
+
let output = TRACKING_PATTERNS + "\n\n" + COMMON_MISTAKES + "\n\n";
|
|
2604
|
+
if (framework && CHECKLISTS[framework]) {
|
|
2605
|
+
output += CHECKLISTS[framework];
|
|
2606
|
+
} else {
|
|
2607
|
+
output += GENERAL_CHECKLIST;
|
|
2608
|
+
}
|
|
2609
|
+
return {
|
|
2610
|
+
content: [{ type: "text", text: output }]
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2205
2614
|
// src/index.ts
|
|
2206
2615
|
var apiKey = process.env.EASYFUNNEL_API_KEY;
|
|
2207
2616
|
if (!apiKey) {
|
|
@@ -2233,7 +2642,8 @@ server.setRequestHandler(import_types.ListToolsRequestSchema, async () => ({
|
|
|
2233
2642
|
deleteFunnelDefinition,
|
|
2234
2643
|
updateFunnelDefinition,
|
|
2235
2644
|
setupChatWidgetDefinition,
|
|
2236
|
-
queryConversationsDefinition
|
|
2645
|
+
queryConversationsDefinition,
|
|
2646
|
+
getBestPracticesDefinition
|
|
2237
2647
|
]
|
|
2238
2648
|
}));
|
|
2239
2649
|
server.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
|
|
@@ -2271,6 +2681,8 @@ server.setRequestHandler(import_types.CallToolRequestSchema, async (request) =>
|
|
|
2271
2681
|
return setupChatWidget(client, args);
|
|
2272
2682
|
case "query_conversations":
|
|
2273
2683
|
return queryConversations(client, args);
|
|
2684
|
+
case "get_best_practices":
|
|
2685
|
+
return getBestPractices(args);
|
|
2274
2686
|
default:
|
|
2275
2687
|
throw new Error(`Unknown tool: ${name}`);
|
|
2276
2688
|
}
|