@gurulu/cli 0.3.4 → 0.4.1

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.
Files changed (39) hide show
  1. package/README.md +61 -24
  2. package/dist/api-client.js +1 -1
  3. package/dist/commands/add-server.js +13 -6
  4. package/dist/commands/alerts.d.ts +5 -0
  5. package/dist/commands/alerts.js +43 -15
  6. package/dist/commands/audiences.d.ts +3 -0
  7. package/dist/commands/audiences.js +34 -7
  8. package/dist/commands/events.d.ts +6 -0
  9. package/dist/commands/events.js +182 -1
  10. package/dist/commands/experiments.d.ts +4 -0
  11. package/dist/commands/experiments.js +46 -15
  12. package/dist/commands/funnels.d.ts +17 -0
  13. package/dist/commands/funnels.js +203 -0
  14. package/dist/commands/goals.d.ts +18 -0
  15. package/dist/commands/goals.js +214 -0
  16. package/dist/commands/install.d.ts +8 -0
  17. package/dist/commands/install.js +74 -4
  18. package/dist/commands/sourcemap.d.ts +17 -5
  19. package/dist/commands/sourcemap.js +73 -6
  20. package/dist/commands/watch.d.ts +45 -0
  21. package/dist/commands/watch.js +258 -0
  22. package/dist/frameworks/detect.js +29 -7
  23. package/dist/index.js +158 -13
  24. package/package.json +1 -1
  25. package/scripts/gurulu-agentic-install.mjs +225 -0
  26. package/scripts/gurulu-scan.lib.cjs +539 -19
  27. package/scripts/patches/astro.patch.cjs +1 -0
  28. package/scripts/patches/auto-instrument/hono.cjs +381 -0
  29. package/scripts/patches/auto-instrument/index.cjs +2 -0
  30. package/scripts/patches/auto-instrument/nextjs-app-router.cjs +13 -4
  31. package/scripts/patches/express.patch.cjs +2 -2
  32. package/scripts/patches/fastify.patch.cjs +1 -0
  33. package/scripts/patches/nestjs.patch.cjs +1 -0
  34. package/scripts/patches/nextjs-app-router.patch.cjs +2 -2
  35. package/scripts/patches/nextjs-pages.patch.cjs +1 -0
  36. package/scripts/patches/remix.patch.cjs +1 -0
  37. package/scripts/patches/sveltekit.patch.cjs +1 -0
  38. package/scripts/patches/vite-react.patch.cjs +1 -0
  39. package/scripts/patches/vue.patch.cjs +1 -0
@@ -0,0 +1,381 @@
1
+ // scripts/patches/auto-instrument/hono.cjs — Phase 20 W1 A4.
2
+ //
3
+ // Hono supports several idiomatic route registration shapes:
4
+ //
5
+ // app.get('/users', (c) => { ... });
6
+ // app.post('/orders', async (c) => { ... });
7
+ // const api = new Hono(); api.get('/users', handler);
8
+ // app.get('/health', (c) => c.json({ ok: true })).post('/data', async (c) => { ... });
9
+ //
10
+ // We search the usual server entry files plus `routes/**` for a matching
11
+ // registration, parse the AST, and inject `gurulu.track(...)` into the
12
+ // handler body before the last return.
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const ast = require('./ast-helper.cjs');
18
+ const singleton = require('./singleton-helper.cjs');
19
+
20
+ const NAME = 'auto-instrument-hono';
21
+ const SUPPORTED = ['hono'];
22
+ const MARKER = '// @gurulu-instrumented';
23
+ const IMPORT_LINE_ESM = "import { gurulu } from '@/lib/gurulu';";
24
+
25
+ const CANDIDATE_ENTRIES = [
26
+ 'app.js',
27
+ 'server.js',
28
+ 'index.js',
29
+ 'src/app.js',
30
+ 'src/server.js',
31
+ 'src/index.js',
32
+ 'src/app.ts',
33
+ 'src/server.ts',
34
+ 'src/index.ts',
35
+ 'src/main.ts',
36
+ 'src/main.js',
37
+ ];
38
+
39
+ function parseEventRoute(routeStr) {
40
+ if (!routeStr || typeof routeStr !== 'string') return null;
41
+ const m = routeStr.trim().match(/^([A-Z]+)\s+(\/.*)$/);
42
+ if (!m) return null;
43
+ return { method: m[1].toUpperCase(), urlPath: m[2].replace(/\/+$/, '') || '/' };
44
+ }
45
+
46
+ function walkRoutes(repoRoot) {
47
+ const results = [];
48
+ const dirs = ['routes', 'src/routes', 'src/api', 'app', 'src/app'];
49
+ for (const d of dirs) {
50
+ const abs = path.join(repoRoot, d);
51
+ if (!fs.existsSync(abs)) continue;
52
+ for (const entry of fs.readdirSync(abs, { withFileTypes: true })) {
53
+ if (entry.isFile() && /\.(js|ts|mjs|cjs)$/.test(entry.name)) {
54
+ results.push(path.posix.join(d, entry.name));
55
+ }
56
+ }
57
+ }
58
+ return results;
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // AST path
63
+ // ---------------------------------------------------------------------------
64
+
65
+ /**
66
+ * Find all `app.<method>('path', ..., handler)` or `api.<method>(...)`
67
+ * or `router.<method>(...)` call expressions matching the given method +
68
+ * urlPath. Hono apps are typically named `app`, `api`, `router`, or
69
+ * `server`. Returns an array of `{ fn, body }` entries.
70
+ */
71
+ function findHonoHandlers(tree, method, urlPath) {
72
+ const t = ast.t;
73
+ const results = [];
74
+ const lowerMethod = method.toLowerCase();
75
+ const HONO_IDENTIFIERS = new Set(['app', 'api', 'router', 'server']);
76
+
77
+ ast.traverse(tree, {
78
+ CallExpression(p) {
79
+ const callee = p.node.callee;
80
+ if (!t.isMemberExpression(callee)) return;
81
+ if (!t.isIdentifier(callee.property) || callee.property.name !== lowerMethod) return;
82
+
83
+ // The object can be a simple identifier (app.get) or a chained call
84
+ // expression (app.get('/a', h).post('/b', h) — the object of `.post`
85
+ // is the preceding CallExpression). We accept identifiers from the
86
+ // known set, and also any CallExpression (chained).
87
+ const obj = callee.object;
88
+ const isKnownIdent = t.isIdentifier(obj) && HONO_IDENTIFIERS.has(obj.name);
89
+ const isChained = t.isCallExpression(obj);
90
+ if (!isKnownIdent && !isChained) return;
91
+
92
+ const args = p.node.arguments;
93
+ if (args.length < 2) return;
94
+ if (!t.isStringLiteral(args[0]) || args[0].value !== urlPath) return;
95
+
96
+ // The final argument should be the handler function.
97
+ const handler = args[args.length - 1];
98
+ if (
99
+ (t.isArrowFunctionExpression(handler) || t.isFunctionExpression(handler)) &&
100
+ t.isBlockStatement(handler.body)
101
+ ) {
102
+ results.push({ fn: handler, body: handler.body });
103
+ }
104
+ },
105
+ });
106
+ return results;
107
+ }
108
+
109
+ function astInstrumentFile(source, method, urlPath, events) {
110
+ const tree = ast.parseSource(source);
111
+ const handlers = findHonoHandlers(tree, method, urlPath);
112
+ if (handlers.length === 0) {
113
+ return { ok: false, reason: 'handler-not-found' };
114
+ }
115
+ const target = handlers[0];
116
+ if (ast.hasInstrumentedMarker(target.fn)) {
117
+ return {
118
+ ok: true,
119
+ after: source,
120
+ instrumented: [],
121
+ skipped: events.map((e) => ({ event: e.name, reason: 'already-instrumented' })),
122
+ changed: false,
123
+ };
124
+ }
125
+ const stmts = events.map((e) => ast.buildTrackStatement(e.name, e.autoProperties));
126
+ ast.injectTrackBeforeLastReturn(target.fn, target.body, stmts);
127
+ ast.ensureGuruluImport(tree);
128
+ const after = ast.generateSource(tree, source);
129
+ return {
130
+ ok: true,
131
+ after,
132
+ instrumented: events.map((e) => e.name),
133
+ skipped: [],
134
+ changed: true,
135
+ };
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Regex fallback
140
+ // ---------------------------------------------------------------------------
141
+
142
+ function regexFindHandlerStart(source, method, urlPath) {
143
+ const lower = method.toLowerCase();
144
+ const escapedPath = urlPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
145
+ const re = new RegExp(
146
+ `(?:app|api|router|server)\\.${lower}\\s*\\(\\s*['"\`]${escapedPath}['"\`]\\s*,`,
147
+ );
148
+ const m = re.exec(source);
149
+ if (!m) return null;
150
+ let i = m.index + m[0].length;
151
+ const tail = source.slice(i);
152
+ const arrowIdx = tail.search(/=>\s*\{/);
153
+ const funcIdx = tail.search(/function\s*[^(]*\([^)]*\)\s*\{/);
154
+ let bodyOpen = -1;
155
+ if (arrowIdx !== -1 && (funcIdx === -1 || arrowIdx < funcIdx)) {
156
+ bodyOpen = i + tail.indexOf('{', arrowIdx);
157
+ } else if (funcIdx !== -1) {
158
+ bodyOpen = i + tail.indexOf('{', funcIdx);
159
+ }
160
+ if (bodyOpen === -1) return null;
161
+ let depth = 0;
162
+ for (let j = bodyOpen; j < source.length; j++) {
163
+ const c = source[j];
164
+ if (c === '{') depth++;
165
+ else if (c === '}') {
166
+ depth--;
167
+ if (depth === 0) return { start: bodyOpen, end: j };
168
+ }
169
+ }
170
+ return null;
171
+ }
172
+
173
+ function regexFindLastResponseCall(source, start, end) {
174
+ const snippet = source.slice(start, end);
175
+ // Hono response patterns: c.json(), c.text(), c.html(), c.body(),
176
+ // c.redirect(), c.notFound()
177
+ const patterns = [
178
+ /c\.json\s*\(/g,
179
+ /c\.text\s*\(/g,
180
+ /c\.html\s*\(/g,
181
+ /c\.body\s*\(/g,
182
+ /c\.redirect\s*\(/g,
183
+ /c\.notFound\s*\(/g,
184
+ /return\s+c\./g,
185
+ ];
186
+ let lastIdx = -1;
187
+ for (const re of patterns) {
188
+ let m;
189
+ while ((m = re.exec(snippet)) !== null) {
190
+ if (m.index > lastIdx) lastIdx = m.index;
191
+ }
192
+ }
193
+ if (lastIdx === -1) return null;
194
+ const absoluteIdx = start + lastIdx;
195
+ let lineStart = absoluteIdx;
196
+ while (lineStart > 0 && source[lineStart - 1] !== '\n') lineStart--;
197
+ const indentMatch = source.slice(lineStart, absoluteIdx).match(/^(\s*)/);
198
+ const indent = (indentMatch && indentMatch[1]) || ' ';
199
+ return { insertAt: lineStart, indent };
200
+ }
201
+
202
+ function regexEnsureImport(source) {
203
+ if (source.includes(IMPORT_LINE_ESM)) return source;
204
+ const importRegex = /^(?:import[\s\S]*?;\s*\n)+/m;
205
+ const m = source.match(importRegex);
206
+ if (m) {
207
+ const end = m.index + m[0].length;
208
+ return source.slice(0, end) + IMPORT_LINE_ESM + '\n' + source.slice(end);
209
+ }
210
+ return IMPORT_LINE_ESM + '\n' + source;
211
+ }
212
+
213
+ function regexBuildTrackCall(eventName, indent) {
214
+ const safeName = JSON.stringify(eventName);
215
+ return `${indent}${MARKER} ${eventName}\n${indent}gurulu.track(${safeName}, {});\n`;
216
+ }
217
+
218
+ function regexInstrumentFile(before, method, urlPath, events) {
219
+ const body = regexFindHandlerStart(before, method, urlPath);
220
+ if (!body) return { ok: false, reason: 'handler-not-found' };
221
+ const snippet = before.slice(body.start, body.end);
222
+ const needed = [];
223
+ const skipped = [];
224
+ for (const e of events) {
225
+ if (snippet.includes(`${MARKER} ${e.name}`)) {
226
+ skipped.push({ event: e.name, reason: 'already-instrumented' });
227
+ } else {
228
+ needed.push(e);
229
+ }
230
+ }
231
+ if (needed.length === 0) return { ok: true, after: before, instrumented: [], skipped, changed: false };
232
+ const ret = regexFindLastResponseCall(before, body.start, body.end);
233
+ if (!ret) return { ok: false, reason: 'no-response-found' };
234
+ const block = needed.map((e) => regexBuildTrackCall(e.name, ret.indent)).join('');
235
+ let after = before.slice(0, ret.insertAt) + block + before.slice(ret.insertAt);
236
+ after = regexEnsureImport(after);
237
+ return {
238
+ ok: true,
239
+ after,
240
+ instrumented: needed.map((e) => e.name),
241
+ skipped,
242
+ changed: true,
243
+ };
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // Glue
248
+ // ---------------------------------------------------------------------------
249
+
250
+ function resolveRouteFile(ctx, method, urlPath) {
251
+ const seen = new Set();
252
+ const candidates = [
253
+ ...CANDIDATE_ENTRIES,
254
+ ...walkRoutes(ctx.repoRoot),
255
+ ].filter((rel) => {
256
+ if (seen.has(rel)) return false;
257
+ seen.add(rel);
258
+ return fs.existsSync(path.join(ctx.repoRoot, rel));
259
+ });
260
+ for (const rel of candidates) {
261
+ const src = fs.readFileSync(path.join(ctx.repoRoot, rel), 'utf8');
262
+ // Cheap pre-filter: must contain `.${method.toLowerCase()}(`.
263
+ if (!src.includes(`.${method.toLowerCase()}(`)) continue;
264
+ try {
265
+ const tree = ast.parseSource(src);
266
+ const handlers = findHonoHandlers(tree, method, urlPath);
267
+ if (handlers.length > 0) return { relPath: rel };
268
+ } catch (_) {
269
+ // Regex check as last-ditch.
270
+ if (regexFindHandlerStart(src, method, urlPath)) return { relPath: rel };
271
+ }
272
+ }
273
+ return null;
274
+ }
275
+
276
+ function instrumentEvents(ctx, events) {
277
+ const helper = singleton.ensureSingletonHelper(ctx, 'hono');
278
+ const changes = [...helper.changes];
279
+ const notes = [...helper.notes];
280
+ let eventsInstrumented = 0;
281
+ let eventsSkipped = 0;
282
+
283
+ if (helper.collision) {
284
+ return {
285
+ changes: [],
286
+ notes,
287
+ filesModified: 0,
288
+ eventsInstrumented: 0,
289
+ eventsSkipped: events ? events.length : 0,
290
+ collision: true,
291
+ };
292
+ }
293
+
294
+ const groups = new Map();
295
+ for (const e of events || []) {
296
+ const parsed = parseEventRoute(e && e.source && e.source.route);
297
+ if (!parsed) {
298
+ notes.push(`skip:${e && e.name}:no-source-route`);
299
+ eventsSkipped++;
300
+ continue;
301
+ }
302
+ const resolved = resolveRouteFile(ctx, parsed.method, parsed.urlPath);
303
+ if (!resolved) {
304
+ notes.push(`skip:${e.name}:route-not-found`);
305
+ eventsSkipped++;
306
+ continue;
307
+ }
308
+ const key = `${resolved.relPath}::${parsed.method}::${parsed.urlPath}`;
309
+ if (!groups.has(key)) {
310
+ groups.set(key, {
311
+ relPath: resolved.relPath,
312
+ method: parsed.method,
313
+ urlPath: parsed.urlPath,
314
+ events: [],
315
+ });
316
+ }
317
+ groups.get(key).events.push(e);
318
+ }
319
+
320
+ for (const group of groups.values()) {
321
+ const abs = path.join(ctx.repoRoot, group.relPath);
322
+ const before = fs.readFileSync(abs, 'utf8');
323
+ let res;
324
+ try {
325
+ res = astInstrumentFile(before, group.method, group.urlPath, group.events);
326
+ } catch (err) {
327
+ const msg = (err && err.message) || String(err);
328
+ // eslint-disable-next-line no-console
329
+ console.warn(`[auto-instrument] patch.fallback ${group.relPath}: ${msg}`);
330
+ notes.push(`patch.fallback:${group.relPath}:${msg}`);
331
+ res = regexInstrumentFile(before, group.method, group.urlPath, group.events);
332
+ }
333
+ if (!res.ok) {
334
+ for (const e of group.events) {
335
+ notes.push(`skip:${e.name}:${res.reason || 'instrument-failed'}`);
336
+ eventsSkipped++;
337
+ }
338
+ continue;
339
+ }
340
+ for (const s of res.skipped || []) {
341
+ notes.push(`skip:${s.event}:${s.reason}`);
342
+ eventsSkipped++;
343
+ }
344
+ if (!res.changed) continue;
345
+ const existing = changes.find((c) => c.relPath === group.relPath && c.type === 'auto-instrument');
346
+ if (existing) {
347
+ existing.after = res.after;
348
+ } else {
349
+ changes.push({
350
+ relPath: group.relPath,
351
+ before,
352
+ after: res.after,
353
+ reason: `auto-instrument-${group.method}`,
354
+ type: 'auto-instrument',
355
+ });
356
+ }
357
+ eventsInstrumented += (res.instrumented || []).length;
358
+ }
359
+
360
+ const filesModified = changes.filter((c) => c.type === 'auto-instrument').length;
361
+ return { changes, notes, filesModified, eventsInstrumented, eventsSkipped, collision: false };
362
+ }
363
+
364
+ function ensureSingletonHelper(ctx) {
365
+ return singleton.ensureSingletonHelper(ctx, 'hono');
366
+ }
367
+
368
+ module.exports = {
369
+ name: NAME,
370
+ supportedFrameworks: SUPPORTED,
371
+ ensureSingletonHelper,
372
+ instrumentEvents,
373
+ _internals: {
374
+ parseEventRoute,
375
+ resolveRouteFile,
376
+ astInstrumentFile,
377
+ regexInstrumentFile,
378
+ walkRoutes,
379
+ findHonoHandlers,
380
+ },
381
+ };
@@ -16,6 +16,7 @@ const remix = require('./remix.cjs');
16
16
  const sveltekit = require('./sveltekit.cjs');
17
17
  const astro = require('./astro.cjs');
18
18
  const fastify = require('./fastify.cjs');
19
+ const hono = require('./hono.cjs');
19
20
  const vue = require('./vue.cjs');
20
21
  const singleton = require('./singleton-helper.cjs');
21
22
 
@@ -29,6 +30,7 @@ const MODULES = [
29
30
  sveltekit,
30
31
  astro,
31
32
  fastify,
33
+ hono,
32
34
  vue,
33
35
  ];
34
36
 
@@ -70,7 +70,7 @@ function astInstrumentFile(source, method, events, opts = {}) {
70
70
  };
71
71
  }
72
72
  const trackStmts = events.map((e) =>
73
- ast.buildTrackStatement(e.name, e.autoProperties || opts.autoProperties && e.extractedProperties),
73
+ ast.buildTrackStatement(e.name, e.extractedProperties || e.autoProperties),
74
74
  );
75
75
  const injected = ast.injectTrackBeforeLastReturn(target.fn, target.body, trackStmts);
76
76
  if (!injected) {
@@ -147,11 +147,20 @@ function regexFindLastReturnInBody(source, start, end) {
147
147
  return { insertAt: lineStart, indent };
148
148
  }
149
149
 
150
- function regexBuildTrackCall(eventName, indent) {
150
+ function regexBuildTrackCall(eventName, indent, extractedProperties) {
151
151
  const safeName = JSON.stringify(eventName);
152
+ let propsStr = '{}';
153
+ if (Array.isArray(extractedProperties) && extractedProperties.length > 0) {
154
+ const entries = extractedProperties
155
+ .filter((p) => p && p.name && p.source)
156
+ .map((p) => `${p.name}: ${p.source}`);
157
+ if (entries.length > 0) {
158
+ propsStr = `{ ${entries.join(', ')} }`;
159
+ }
160
+ }
152
161
  return (
153
162
  `${indent}${MARKER} ${eventName}\n` +
154
- `${indent}gurulu.track(${safeName}, {});\n`
163
+ `${indent}gurulu.track(${safeName}, ${propsStr});\n`
155
164
  );
156
165
  }
157
166
 
@@ -173,7 +182,7 @@ function regexInstrumentFile(before, method, events) {
173
182
  if (needed.length === 0) return { ok: true, after: before, instrumented: [], skipped, changed: false };
174
183
  const ret = regexFindLastReturnInBody(before, body.start, body.end);
175
184
  if (!ret) return { ok: false, reason: 'no-return-found' };
176
- const block = needed.map((e) => regexBuildTrackCall(e.name, ret.indent)).join('');
185
+ const block = needed.map((e) => regexBuildTrackCall(e.name, ret.indent, e.extractedProperties)).join('');
177
186
  let after = before.slice(0, ret.insertAt) + block + before.slice(ret.insertAt);
178
187
  after = regexEnsureImport(after).source;
179
188
  return {
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Detects an Express server (`app.js`, `server.js`, `src/index.js` with the
4
4
  // idiomatic `const app = express()` signature) and appends:
5
- // - A proxy route that serves /gurulu-tracker.js from the Gurulu CDN
5
+ // - A proxy route that serves /gurulu-tracker from the Gurulu CDN
6
6
  // - A `res.locals.guruluConfig` middleware exposing siteId / tenantId
7
7
 
8
8
  const fs = require('fs');
@@ -59,7 +59,7 @@ function buildMiddleware(appVar, injection) {
59
59
  ` };\n` +
60
60
  ` next();\n` +
61
61
  `});\n` +
62
- `${appVar}.use('/gurulu-tracker.js', (req, res) => {\n` +
62
+ `${appVar}.use('/gurulu-tracker', (req, res) => {\n` +
63
63
  ` res.set('Content-Type', 'application/javascript');\n` +
64
64
  ` res.send(\`// Gurulu tracker proxy — site=\${${JSON.stringify(injection.siteId)}} data-gurulu-publishable-key=\${${JSON.stringify(injection.publishableKey || '')}}\\n` +
65
65
  `window.gurulu = window.gurulu || { queue: [] };\`);\n` +
@@ -68,6 +68,7 @@ function buildScriptTag(injection) {
68
68
  ` src="${injection.scriptSrc}"\n` +
69
69
  ` data-gurulu-site-id="${injection.siteId}"\n` +
70
70
  ` data-gurulu-tenant-id="${injection.tenantId}"${pkAttr}\n` +
71
+ ` data-features="errors,replay,advanced"\n` +
71
72
  ` ${MARKER}\n` +
72
73
  ` async\n` +
73
74
  ` ></script>\n`
@@ -70,6 +70,7 @@ function buildScriptTag(injection) {
70
70
  ` src="${injection.scriptSrc}"\n` +
71
71
  ` data-gurulu-site-id="${injection.siteId}"\n` +
72
72
  ` data-gurulu-tenant-id="${injection.tenantId}"${pkAttr}\n` +
73
+ ` data-features="errors,replay,advanced"\n` +
73
74
  ` ${MARKER}\n` +
74
75
  ` async\n` +
75
76
  ` ></script>\n`
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Detects a Next.js App Router layout (`src/app/layout.tsx` or `app/layout.tsx`)
4
4
  // and injects a <Script> tag loaded via next/script that points to the Gurulu
5
- // Web SDK tracker (`/gurulu-tracker.js` by default).
5
+ // Web SDK tracker (`https://gurulu.io/t.js` by default).
6
6
  //
7
7
  // Regex-based: fast alpha, good enough for most idiomatic RootLayouts.
8
8
 
@@ -40,7 +40,7 @@ function buildScriptTag(injection) {
40
40
  ` data-gurulu-site-id=${JSON.stringify(injection.siteId)}\n` +
41
41
  ` data-gurulu-tenant-id=${JSON.stringify(injection.tenantId)}\n` +
42
42
  pkAttr +
43
- ` data-features="errors"\n` +
43
+ ` data-features="errors,replay,advanced"\n` +
44
44
  ` ${MARKER}\n` +
45
45
  ` />\n`
46
46
  );
@@ -54,6 +54,7 @@ function buildHook(injection) {
54
54
  ` s.setAttribute('data-gurulu-site-id', ${JSON.stringify(injection.siteId)});\n` +
55
55
  ` s.setAttribute('data-gurulu-tenant-id', ${JSON.stringify(injection.tenantId)});\n` +
56
56
  pkLine +
57
+ ` s.setAttribute('data-features', 'errors,replay,advanced');\n` +
57
58
  ` document.head.appendChild(s);\n` +
58
59
  ` }, []);\n`
59
60
  );
@@ -36,6 +36,7 @@ function buildScriptTag(injection) {
36
36
  ` data-gurulu-site-id=${JSON.stringify(injection.siteId)}\n` +
37
37
  ` data-gurulu-tenant-id=${JSON.stringify(injection.tenantId)}\n` +
38
38
  pkAttr +
39
+ ` data-features="errors,replay,advanced"\n` +
39
40
  ` ${MARKER}\n` +
40
41
  ` async\n` +
41
42
  ` />\n`
@@ -33,6 +33,7 @@ function buildScriptTag(injection) {
33
33
  ` src="${injection.scriptSrc}"\n` +
34
34
  ` data-gurulu-site-id="${injection.siteId}"\n` +
35
35
  ` data-gurulu-tenant-id="${injection.tenantId}"${pkAttr}\n` +
36
+ ` data-features="errors,replay,advanced"\n` +
36
37
  ` ${MARKER}\n` +
37
38
  ` async\n` +
38
39
  ` ></script>\n`
@@ -35,6 +35,7 @@ function buildBootstrap(injection) {
35
35
  ` s.setAttribute('data-gurulu-install', '1');\n` +
36
36
  ` s.setAttribute('data-gurulu-site-id', ${JSON.stringify(injection.siteId)});\n` +
37
37
  pkLine +
38
+ ` s.setAttribute('data-features', 'errors,replay,advanced');\n` +
38
39
  ` document.head.appendChild(s);\n` +
39
40
  `}\n`
40
41
  );
@@ -48,6 +48,7 @@ function buildScriptTag(injection) {
48
48
  ` src="${injection.scriptSrc}"\n` +
49
49
  ` data-gurulu-site-id="${injection.siteId}"\n` +
50
50
  ` data-gurulu-tenant-id="${injection.tenantId}"${pkAttr}\n` +
51
+ ` data-features="errors,replay,advanced"\n` +
51
52
  ` ${MARKER}\n` +
52
53
  ` async\n` +
53
54
  ` ></script>\n`