@agent-native/core 0.98.3 → 0.98.4
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +13 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/analytics.ts +12 -0
- package/corpus/core/src/client/session-replay.ts +612 -26
- package/corpus/core/src/client/settings/DemoModeSection.tsx +3 -3
- package/corpus/core/src/demo/actions/toggle-demo-mode.ts +2 -2
- package/corpus/core/src/demo/fetch-interceptor.ts +25 -2
- package/corpus/core/src/demo/redact.ts +58 -9
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +46 -4
- package/corpus/templates/analytics/app/global.css +4 -62
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +225 -52
- package/corpus/templates/analytics/changelog/2026-07-12-fixed-session-replays-that-appeared-ultra-wide-hid-recorded-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-session-replays-load-faster-in-demo-mode-while-visitor-email.md +6 -0
- package/corpus/templates/calendar/app/components/calendar/DeleteEventDialog.tsx +11 -2
- package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +14 -2
- package/corpus/templates/calendar/changelog/2026-07-12-calendar-responses-can-now-be-saved-with-command-enter-or-ct.md +6 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +11 -0
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/session-replay.d.ts +12 -3
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +488 -23
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/settings/DemoModeSection.js +1 -1
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/demo/actions/toggle-demo-mode.js +2 -2
- package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
- package/dist/demo/fetch-interceptor.d.ts +1 -0
- package/dist/demo/fetch-interceptor.d.ts.map +1 -1
- package/dist/demo/fetch-interceptor.js +19 -2
- package/dist/demo/fetch-interceptor.js.map +1 -1
- package/dist/demo/redact.d.ts +7 -0
- package/dist/demo/redact.d.ts.map +1 -1
- package/dist/demo/redact.js +19 -10
- package/dist/demo/redact.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -64,9 +64,9 @@ export function DemoModeSection() {
|
|
|
64
64
|
Enable demo mode
|
|
65
65
|
</div>
|
|
66
66
|
<p className="text-[10px] text-muted-foreground mt-0.5">
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
Anonymize emails in the UI and reshape supported dashboard charts for
|
|
68
|
+
presentations. Agent-visible emails and numbers are also anonymized.
|
|
69
|
+
Names, labels, IDs, and structure stay intact.
|
|
70
70
|
</p>
|
|
71
71
|
{forced && (
|
|
72
72
|
<p className="text-[10px] text-muted-foreground mt-0.5">
|
|
@@ -5,7 +5,7 @@ import { writeAppState } from "../../application-state/script-helpers.js";
|
|
|
5
5
|
|
|
6
6
|
export default defineAction({
|
|
7
7
|
description:
|
|
8
|
-
"Turn demo mode on or off. When demo mode is on, the
|
|
8
|
+
"Turn demo mode on or off. When demo mode is on, the UI replaces email addresses with realistic fake data and supported dashboards reshape chart values for presentations. Agent-visible results replace emails and numbers while keeping names, free text, labels, IDs, dates, and structure intact. Use when the user asks to 'hide my data', 'turn on demo mode', 'anonymize this for a screen share / recording', or similar. This is the same toggle as the Demo mode switch in settings.",
|
|
9
9
|
schema: z.object({
|
|
10
10
|
enabled: z
|
|
11
11
|
.boolean()
|
|
@@ -16,7 +16,7 @@ export default defineAction({
|
|
|
16
16
|
return {
|
|
17
17
|
enabled,
|
|
18
18
|
message: enabled
|
|
19
|
-
? "Demo mode is ON — emails and
|
|
19
|
+
? "Demo mode is ON — UI emails are anonymized and supported dashboard charts are reshaped for presentation. Agent-visible emails and numbers are also anonymized. Names, free text, labels, IDs, and structure are preserved."
|
|
20
20
|
: "Demo mode is OFF — real data is shown again.",
|
|
21
21
|
};
|
|
22
22
|
},
|
|
@@ -46,6 +46,23 @@ const SKIP_SUBSTRINGS = [
|
|
|
46
46
|
"/_agent-native/runs",
|
|
47
47
|
];
|
|
48
48
|
|
|
49
|
+
// Raw rrweb payloads are playback data, not ordinary app UI records. Walking
|
|
50
|
+
// and cloning their huge DOM/event trees on the main thread stalls playback
|
|
51
|
+
// and can corrupt replay fidelity. Small list/summary/manifest responses stay
|
|
52
|
+
// eligible so rendered visitor identities are still anonymized.
|
|
53
|
+
const RAW_REPLAY_PAYLOAD_RE =
|
|
54
|
+
/\/api\/session-replay\/recordings\/[^/?#]+\/(?:chunks(?:\/[^/?#]+)?|events)(?:[/?#]|$)/;
|
|
55
|
+
const RAW_AGENT_REPLAY_EVENTS_RE =
|
|
56
|
+
/\/api\/session-replay\/agent-events\.json(?:[?#]|$)/;
|
|
57
|
+
|
|
58
|
+
export function shouldSkipDemoResponseRedaction(url: string): boolean {
|
|
59
|
+
return (
|
|
60
|
+
SKIP_SUBSTRINGS.some((substring) => url.includes(substring)) ||
|
|
61
|
+
RAW_REPLAY_PAYLOAD_RE.test(url) ||
|
|
62
|
+
RAW_AGENT_REPLAY_EVENTS_RE.test(url)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
let installed = false;
|
|
50
67
|
let demoEnabled = false;
|
|
51
68
|
let originalFetch: typeof fetch | null = null;
|
|
@@ -148,7 +165,7 @@ export function ensureDemoModeFetchInterceptor(): void {
|
|
|
148
165
|
|
|
149
166
|
try {
|
|
150
167
|
const url = urlOf(input);
|
|
151
|
-
if (
|
|
168
|
+
if (shouldSkipDemoResponseRedaction(url)) return res;
|
|
152
169
|
|
|
153
170
|
// Only buffered, finite JSON. SSE / streaming / chunked-forever bodies
|
|
154
171
|
// never reach `redactDemoData`: streaming content-types are excluded,
|
|
@@ -167,7 +184,13 @@ export function ensureDemoModeFetchInterceptor(): void {
|
|
|
167
184
|
if (res.bodyUsed) return res;
|
|
168
185
|
|
|
169
186
|
const data = await withTimeout(res.clone().json(), 3_000);
|
|
170
|
-
|
|
187
|
+
// Frontend reads only need identity privacy. Dashboard charts apply
|
|
188
|
+
// their purpose-built demo trend transform at render time, so mutating
|
|
189
|
+
// every numeric field in every JSON response is unnecessary work.
|
|
190
|
+
const redacted = redactDemoData(data, {
|
|
191
|
+
redactNumbers: false,
|
|
192
|
+
redactProtectedEmails: true,
|
|
193
|
+
});
|
|
171
194
|
|
|
172
195
|
const headers = new Headers(res.headers);
|
|
173
196
|
// Body is re-serialized — these would be wrong now.
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
|
|
15
15
|
export interface RedactOptions {
|
|
16
16
|
salt?: string;
|
|
17
|
+
/** Redact numeric values. Defaults to true for backward compatibility. */
|
|
18
|
+
redactNumbers?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Redact emails stored in otherwise protected structural fields such as
|
|
21
|
+
* `userId` and `userKey`. Intended for display-only frontend responses.
|
|
22
|
+
*/
|
|
23
|
+
redactProtectedEmails?: boolean;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
/* ------------------------------------------------------------------ *
|
|
@@ -532,18 +539,29 @@ function transformNumbers(text: string, salt: string): string {
|
|
|
532
539
|
* Public: string redactor
|
|
533
540
|
* ------------------------------------------------------------------ */
|
|
534
541
|
|
|
535
|
-
function redactDemoStringInternal(
|
|
542
|
+
function redactDemoStringInternal(
|
|
543
|
+
text: string,
|
|
544
|
+
salt: string,
|
|
545
|
+
redactNumbers: boolean,
|
|
546
|
+
): string {
|
|
536
547
|
if (typeof text !== "string" || text.length === 0) return text;
|
|
548
|
+
if (!text.includes("@") && (!redactNumbers || !/[\d$€£¥]/.test(text))) {
|
|
549
|
+
return text;
|
|
550
|
+
}
|
|
537
551
|
|
|
538
552
|
const { text: masked, restore } = protect(text);
|
|
539
553
|
let out = masked;
|
|
540
554
|
out = transformEmails(out, salt);
|
|
541
|
-
out = transformNumbers(out, salt);
|
|
555
|
+
if (redactNumbers) out = transformNumbers(out, salt);
|
|
542
556
|
return unprotect(out, restore);
|
|
543
557
|
}
|
|
544
558
|
|
|
545
559
|
export function redactDemoString(text: string, opts?: RedactOptions): string {
|
|
546
|
-
return redactDemoStringInternal(
|
|
560
|
+
return redactDemoStringInternal(
|
|
561
|
+
text,
|
|
562
|
+
opts?.salt ?? "",
|
|
563
|
+
opts?.redactNumbers !== false,
|
|
564
|
+
);
|
|
547
565
|
}
|
|
548
566
|
|
|
549
567
|
/* ------------------------------------------------------------------ *
|
|
@@ -586,6 +604,8 @@ function walk(
|
|
|
586
604
|
salt: string,
|
|
587
605
|
depth: number,
|
|
588
606
|
seen: WeakSet<object>,
|
|
607
|
+
redactNumbers: boolean,
|
|
608
|
+
redactProtectedEmails: boolean,
|
|
589
609
|
): unknown {
|
|
590
610
|
if (depth > MAX_DEPTH) return value;
|
|
591
611
|
|
|
@@ -594,11 +614,11 @@ function walk(
|
|
|
594
614
|
const t = typeof value;
|
|
595
615
|
|
|
596
616
|
if (t === "string") {
|
|
597
|
-
return redactDemoStringInternal(value as string, salt);
|
|
617
|
+
return redactDemoStringInternal(value as string, salt, redactNumbers);
|
|
598
618
|
}
|
|
599
619
|
|
|
600
620
|
if (t === "number") {
|
|
601
|
-
return redactNumberLeaf(value as number, salt);
|
|
621
|
+
return redactNumbers ? redactNumberLeaf(value as number, salt) : value;
|
|
602
622
|
}
|
|
603
623
|
|
|
604
624
|
if (t === "boolean" || t === "bigint" || t === "function" || t === "symbol") {
|
|
@@ -610,7 +630,9 @@ function walk(
|
|
|
610
630
|
if (Array.isArray(value)) {
|
|
611
631
|
if (seen.has(value)) return value;
|
|
612
632
|
seen.add(value);
|
|
613
|
-
const out = value.map((item) =>
|
|
633
|
+
const out = value.map((item) =>
|
|
634
|
+
walk(item, salt, depth + 1, seen, redactNumbers, redactProtectedEmails),
|
|
635
|
+
);
|
|
614
636
|
seen.delete(value);
|
|
615
637
|
return out;
|
|
616
638
|
}
|
|
@@ -629,14 +651,34 @@ function walk(
|
|
|
629
651
|
) {
|
|
630
652
|
// Still recurse into nested structures, but the protected key does
|
|
631
653
|
// not transform its own leaf value.
|
|
632
|
-
out[key] = walk(
|
|
654
|
+
out[key] = walk(
|
|
655
|
+
entry,
|
|
656
|
+
salt,
|
|
657
|
+
depth + 1,
|
|
658
|
+
seen,
|
|
659
|
+
redactNumbers,
|
|
660
|
+
redactProtectedEmails,
|
|
661
|
+
);
|
|
662
|
+
} else if (
|
|
663
|
+
redactProtectedEmails &&
|
|
664
|
+
typeof entry === "string" &&
|
|
665
|
+
entry.includes("@")
|
|
666
|
+
) {
|
|
667
|
+
out[key] = redactDemoStringInternal(entry, salt, false);
|
|
633
668
|
} else {
|
|
634
669
|
// Leaf under a protected key: pass through completely untouched.
|
|
635
670
|
out[key] = entry;
|
|
636
671
|
}
|
|
637
672
|
continue;
|
|
638
673
|
}
|
|
639
|
-
out[key] = walk(
|
|
674
|
+
out[key] = walk(
|
|
675
|
+
entry,
|
|
676
|
+
salt,
|
|
677
|
+
depth + 1,
|
|
678
|
+
seen,
|
|
679
|
+
redactNumbers,
|
|
680
|
+
redactProtectedEmails,
|
|
681
|
+
);
|
|
640
682
|
}
|
|
641
683
|
seen.delete(value);
|
|
642
684
|
return out;
|
|
@@ -648,7 +690,14 @@ function walk(
|
|
|
648
690
|
|
|
649
691
|
export function redactDemoData<T>(value: T, opts?: RedactOptions): T {
|
|
650
692
|
const salt = opts?.salt ?? "";
|
|
651
|
-
return walk(
|
|
693
|
+
return walk(
|
|
694
|
+
value,
|
|
695
|
+
salt,
|
|
696
|
+
0,
|
|
697
|
+
new WeakSet<object>(),
|
|
698
|
+
opts?.redactNumbers !== false,
|
|
699
|
+
opts?.redactProtectedEmails === true,
|
|
700
|
+
) as T;
|
|
652
701
|
}
|
|
653
702
|
|
|
654
703
|
/**
|
|
@@ -102,9 +102,9 @@ agent answers about browser recordings in the Analytics template.
|
|
|
102
102
|
- Wait for all replay chunks (`isComplete`) before constructing the rrweb
|
|
103
103
|
`Replayer`. Progressive chunk publishes should only update the loading bar;
|
|
104
104
|
rebuilding the player mid-load desyncs the scrubber and playhead.
|
|
105
|
-
- Pass events to `Replayer` untouched. rrweb rebuilds them in a sandboxed
|
|
106
|
-
pre-processing DOM, stylesheet, resource, or mutation payloads makes
|
|
107
|
-
diverge from the captured page. In particular, never rewrite `href`, `src`,
|
|
105
|
+
- Pass normal events to `Replayer` untouched. rrweb rebuilds them in a sandboxed
|
|
106
|
+
iframe; pre-processing DOM, stylesheet, resource, or mutation payloads makes
|
|
107
|
+
playback diverge from the captured page. In particular, never rewrite `href`, `src`,
|
|
108
108
|
`_cssText`, CSS `url()`, or Meta URLs to `about:blank`; that exact remediation
|
|
109
109
|
broke historical replay CSS in PR #2040. Handle request privacy at capture or
|
|
110
110
|
the sandbox boundary instead of mutating stored rrweb events. Historical
|
|
@@ -112,15 +112,46 @@ agent answers about browser recordings in the Analytics template.
|
|
|
112
112
|
requests for accurate rendering; the viewer accepts that fidelity tradeoff,
|
|
113
113
|
uses rrweb's script-disabled sandbox plus `referrerpolicy="no-referrer"`, and
|
|
114
114
|
must never add credentials or proxy those URLs through a privileged server.
|
|
115
|
+
- Capture-time URL scrubbing must preserve load-bearing DOM resource attributes:
|
|
116
|
+
`src`, `srcset`, `poster`, `data`, and `href` only on resource links such as
|
|
117
|
+
stylesheets, preloads, and icons. Signed CDN query parameters are part of the
|
|
118
|
+
resource identity; redacting them produces missing CSS, fonts, images, and
|
|
119
|
+
oversized fallback icons. Keep scrubbing Meta/navigation URLs, anchor hrefs,
|
|
120
|
+
and console/network diagnostics. Captured `_cssText` and CSS `@import`/`url()`
|
|
121
|
+
values must remain byte-identical.
|
|
122
|
+
- rrweb rebuilds into an `about:srcdoc` iframe, which inherits the Analytics
|
|
123
|
+
document's CSP. Analytics currently sends no CSP header; if a future change
|
|
124
|
+
adds restrictive `style-src`, `font-src`, or `img-src` directives, verify
|
|
125
|
+
historical replays and resolve external imports/fonts at capture before
|
|
126
|
+
blocking the recorded resource origins. Do not diagnose current font loss as
|
|
127
|
+
CSP without checking the deployed response headers first.
|
|
115
128
|
- Let rrweb own normal iframe sizing via Meta / ViewportResize, and keep the
|
|
116
129
|
outer wrapper in that same coordinate system for fit-to-stage scaling. Some
|
|
117
130
|
legacy recordings contain demonstrably corrupt 4,000–7,500px-wide viewport
|
|
118
131
|
values from ordinary desktop sessions. Only widths of at least 4,000px with
|
|
119
132
|
aspect ratios above 4:1 are recovered to a 16:10 viewport; real 32:9 and
|
|
120
133
|
mobile portrait captures remain untouched. The resize handler applies the
|
|
121
|
-
recovery to **both** rrweb's iframe and the outer stage.
|
|
134
|
+
recovery to **both** rrweb's iframe and the outer stage. This includes the
|
|
135
|
+
historical 3,000–3,999px-wide, sub-1,000px-high malformed shape; do not
|
|
136
|
+
narrow the guard back to only 4,000px+ recordings.
|
|
122
137
|
Never remove this recovery, clamp only one layer, or broadly rewrite event
|
|
123
138
|
geometry: those changes repeatedly recreated the ribbon/clipping regression.
|
|
139
|
+
- The malformed-viewport recovery's sole event-data exception is pointer
|
|
140
|
+
geometry: project MouseMove, TouchMove, Drag, and MouseInteraction x/y into
|
|
141
|
+
the corrected camera and clamp impossible coordinates. Correcting the iframe
|
|
142
|
+
without its paired pointer projection makes rrweb cast valid target ids
|
|
143
|
+
thousands of pixels off-stage. Normal-viewport pointer events must retain
|
|
144
|
+
their original object references and exact coordinates.
|
|
145
|
+
- Keep rrweb's stock cursor stylesheet and its hotspot transform. During
|
|
146
|
+
playback, hide the viewer's native pointer over Analytics' transparent
|
|
147
|
+
click-to-pause overlay so it cannot masquerade as a frozen recorded cursor.
|
|
148
|
+
- Keep rrweb's recorded focus handling enabled. Focus and focus-visible state
|
|
149
|
+
affect menus, forms, and keyboard UX; disabling `triggerFocus` makes a valid
|
|
150
|
+
snapshot diverge from the source page.
|
|
151
|
+
- `insertStyleRules` may suppress known toast/snackbar containers only. Never
|
|
152
|
+
hide generic framework primitives such as
|
|
153
|
+
`[data-radix-popper-content-wrapper]`: Radix dropdowns, selects, tooltips,
|
|
154
|
+
and other real recorded product UI all share that wrapper.
|
|
124
155
|
- Keep the realistic fidelity and malformed-viewport tests in
|
|
125
156
|
`SessionDetailPage.spec.ts`. Do not change their expectations merely to bless
|
|
126
157
|
a new sanitizer or raw ultra-wide sizing; validate the affected replay in a
|
|
@@ -161,3 +192,14 @@ agent answers about browser recordings in the Analytics template.
|
|
|
161
192
|
`.an-mask` or `data-an-mask`.
|
|
162
193
|
- Use `.an-block`, `.an-ignore`, `data-an-block`, or `data-an-ignore` for
|
|
163
194
|
sensitive zones that should not be captured.
|
|
195
|
+
- A definitive upload `409` abandons only the conflicted replay identity and
|
|
196
|
+
immediately starts rrweb again under a fresh per-tab id, producing a new
|
|
197
|
+
Meta + FullSnapshot for long-lived SPA tabs. Recovery is limited to one
|
|
198
|
+
restart until an upload succeeds so a misconfigured endpoint cannot loop;
|
|
199
|
+
Analytics tracks the content-free `session replay upload rejected` lifecycle
|
|
200
|
+
event so conflicts and recovery success are measurable.
|
|
201
|
+
- Do not label an old recording "corrupt" from pointer coordinates, unknown
|
|
202
|
+
mutation node ids, or changing Meta geometry alone. Those shapes can be
|
|
203
|
+
legitimate with scrolling, iframes/shadow DOM, navigation, and resize. A
|
|
204
|
+
historical-artifact notice needs a durable capture/ingest marker or another
|
|
205
|
+
low-false-positive invariant; do not guess from playback heuristics.
|
|
@@ -245,72 +245,14 @@
|
|
|
245
245
|
transform-origin: top left;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
/*
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
inside `.replayer-wrapper`, so it scales with the fit-to-stage transform. */
|
|
253
|
-
.an-replay-stage-root .replayer-mouse {
|
|
254
|
-
position: absolute;
|
|
255
|
-
z-index: 2;
|
|
256
|
-
width: 1.125rem;
|
|
257
|
-
height: 1.5rem;
|
|
258
|
-
border-radius: 0;
|
|
259
|
-
background: transparent;
|
|
260
|
-
background-image: none;
|
|
261
|
-
box-shadow: none;
|
|
262
|
-
transform: none;
|
|
263
|
-
transition:
|
|
264
|
-
left 80ms linear,
|
|
265
|
-
top 80ms linear;
|
|
266
|
-
pointer-events: none;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
.an-replay-stage-root .replayer-mouse::before {
|
|
270
|
-
content: "";
|
|
271
|
-
position: absolute;
|
|
272
|
-
inset: 0;
|
|
273
|
-
background: hsl(0 0% 100%);
|
|
274
|
-
clip-path: polygon(0 0, 0 77%, 30% 57%, 48% 100%, 68% 92%, 50% 51%, 92% 51%);
|
|
275
|
-
filter: drop-shadow(0 0 1px hsl(0 0% 0% / 0.95))
|
|
276
|
-
drop-shadow(0 1px 3px hsl(0 0% 0% / 0.35));
|
|
277
|
-
}
|
|
278
|
-
|
|
248
|
+
/* IMPORTANT: Keep rrweb's stock SVG arrow. Only suppress its permanently
|
|
249
|
+
visible purple ::after disc, which can otherwise read as the cursor itself
|
|
250
|
+
when the arrow is small. The stock .active animation still supplies the
|
|
251
|
+
brief click halo. Do not replace the arrow with an app-colored dot. */
|
|
279
252
|
.an-replay-stage-root .replayer-mouse::after {
|
|
280
|
-
content: "";
|
|
281
|
-
position: absolute;
|
|
282
|
-
left: -0.5rem;
|
|
283
|
-
top: -0.5rem;
|
|
284
|
-
width: 1rem;
|
|
285
|
-
height: 1rem;
|
|
286
|
-
border-radius: 9999px;
|
|
287
|
-
border: 2px solid hsl(217 91% 60%);
|
|
288
|
-
background: transparent;
|
|
289
253
|
opacity: 0;
|
|
290
254
|
}
|
|
291
255
|
|
|
292
|
-
/* rrweb toggles `.active` on mouse interactions; pulse a click ripple. */
|
|
293
|
-
.an-replay-stage-root .replayer-mouse.active::after {
|
|
294
|
-
animation: an-replay-mouse-click 0.4s ease-out 1;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
@keyframes an-replay-mouse-click {
|
|
298
|
-
0% {
|
|
299
|
-
opacity: 0.5;
|
|
300
|
-
transform: scale(1);
|
|
301
|
-
}
|
|
302
|
-
100% {
|
|
303
|
-
opacity: 0;
|
|
304
|
-
transform: scale(3.5);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
.an-replay-stage-root .replayer-mouse-tail {
|
|
309
|
-
position: absolute;
|
|
310
|
-
inset: 0;
|
|
311
|
-
pointer-events: none;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
256
|
.analytics-session-snippet .plan-code-surface {
|
|
315
257
|
border: 0;
|
|
316
258
|
border-radius: 0;
|