Product analytics: replays, pages, errors, insights

Guidely's SDK can also capture how people use your product, not just how they respond to guides:

CapabilityWhat you getDashboard
Session replayDOM recordings of real sessions (rrweb), with console output, network requests, page views, clicks, errors and guide events on the timelineReplays
HeatmapsClick, mouse-movement and scroll heatmaps drawn over a snapshot of the real page, per viewport sizePages → Heatmap
Page analyticsAutomatic page views, time on page, scroll depth, entries/exits, bounce rate, referrers, devices, top clicked elementsPages
Error trackingUncaught errors and unhandled promise rejections, grouped into issues, with stack traces, affected users and a link to the replayErrors
InsightsOrdered conversion funnels and retention cohorts over any event, saved for laterInsights

Everything lives in your Guidely deployment: events and issues in its database, and recordings in its S3 bucket. All of it is tied to the same end users, sessions and guides as the rest of the product. You can, for example, watch replays of people who dropped out of a tour, or see which guide a user saw just before an error.

Customer app ── @guidelytech/* SDK
   │ page_view · page_leave · click · rage_click · exception   → POST /sdk/events  → ProductEvent
   │                                                            (exceptions also → ErrorIssue)
   └ rrweb recorder (lazy chunk, only when replay is on)        → POST /sdk/replay  → S3 object (gzip JSON)
                                                                   + ReplaySession / ReplayChunk index rows
Dashboard: Replays · Pages · Errors · Insights  (aggregate SQL over ProductEvent)

Turning it on

Settings → Privacy & data (admins):

  • **Autocapture**: page views, page leave (time on page and scroll depth), clicks and rage clicks.

    On by default.

  • Error tracking: on by default.
  • **Session replay**: **off** by default, and only available when the server has an S3 bucket

    configured. Choose a sample rate (percentage of sessions recorded), whether console output and network requests are recorded (and whether request headers and bodies are), whether all text is masked, extra block selectors, and how many days recordings are kept (default 30).

No code changes are needed. Every Guidely SDK (React, Next.js, Svelte, SvelteKit, script tag) reads these settings from its configuration and starts capturing within one refresh. A developer can switch a capability off for one installation, but cannot switch on what the project turned off:

createGuidely({
  projectId: "pk_…",
  autocapture: { clicks: false },  // or autocapture: false
  captureErrors: false,
  sessionReplay: false,            // or { maskAllText: true, blockSelectors: [".billing"] }
});

All capture follows the existing analytics switches: nothing is sent when Analytics enabled is off, when the SDK is created with analytics: false, or until consent is granted (consent: "pending" → setConsent("granted")).

Session replay

  • **Recording.** The recorder (rrweb) is a separate chunk that is downloaded only when the project

    has replay on and the session is sampled in. Bundlers code-split it automatically. The script-tag build loads guidely-recorder.js from the same directory as guidely.js.

  • **Sampling** is decided once per session, so a session is either fully recorded or not at all.

    guidely.startSessionRecording() records the current session regardless of the sample rate (useful for "record when a user reports a problem"); stopSessionRecording() stops it.

  • **Sessions** end after the project's session timeout of inactivity (default 30 minutes) or after

    24 hours. The SDK session id is shared by events and recordings, so every event in the Events explorer can open the matching replay.

  • **Uploads** go out every few seconds, gzip-compressed in the browser (`CompressionStream`), and

    are flushed when the page is hidden. Multi-page apps produce one recording that spans page loads.

  • **Player.** Replays → open a recording. Space plays and pauses, ←/→ jump 10 seconds, and you can

    change speed and skip inactivity. The timeline marks page views, rage clicks, errors and guide events (and failed network requests); click one to jump to it. Side tabs show the activity list, console output, network requests (including WebSocket connections and their messages) and session details. ?t=<ms> opens a recording at a point in time (error occurrences link there).

  • **Filters.** User, duration, URL, "has errors", "has rage clicks", a guide (sessions that

    interacted with it), or an error issue (sessions in which it occurred).

  • **Console search.** Find recordings whose console output contains some text, optionally limited

    to levels (errors, warnings, …). Each result lists its matching lines; click one to open the player at the moment it was logged. Inside the player, the Console tab has its own search that jumps between matches.

    • Console output is indexed at upload: one row per distinct line per recording, with a count,

      up to 500 distinct lines per recording. The full output stays in the recording.

    • The index uses Postgres `pg_trgm` for fast substring search; the migration enables the

      extension, which is available on RDS.

Replay alerts

Replays → Alerts notifies you by email or Slack when a finished recording matches conditions. Use it to hear about a rage-click storm on checkout, or a session that logged "payment declined", without watching recordings all day.

  • **Conditions** (every condition you set must match): rage clicks ≥ N; errors ≥ N, or a specific

    error issue; failed network requests ≥ N (5xx, network errors and failed WebSockets); console output containing some text, optionally by level; a visited URL; interaction with a guide; minimum duration.

  • **Timing:** a recording is checked once it has been quiet for 5 minutes. A rule alerts at most

    once per recording, and recordings older than a day aren't checked.

  • **Rate limit:** each rule sends at most *N* alerts an hour (default 10). Further matches are

    counted and reported in the next alert as "+N more recordings matched".

  • **Channels:** email and Slack, the same as error alerts. The message names the user, says what

    matched, and has Watch replay, which opens the recording just before the first matching moment.

  • **Shortcuts:** **Create alert from these filters** on the Replays list turns your current search

    into a rule.

Privacy in recordings

  • Inputs are always masked: typed text, passwords and select values never leave the browser.
  • Text inside elements matching the project's **masked selectors**, `[data-guidely-mask]` or

    .guidely-mask is replaced with *. Turn on Mask all text to mask every text node.

  • Elements matching `[data-guidely-block]` or the **block selectors** are not recorded at all; the

    player shows an empty box of the same size.

  • Canvas, fonts and cross-origin iframes are not recorded.
  • **Network requests** (on by default) record only metadata: method, URL, status, type, timing and

    size. Values of query parameters with sensitive names (token, key, secret, password, auth, code, sig, …) are replaced with [redacted], and requests to Guidely's own API are never recorded.

  • **WebSockets** are recorded with network requests: each connection (redacted URL, protocol, open

    and close times, close code and reason) and its frames' direction, time and size. Up to 200 frames per connection per page load are listed; totals keep counting beyond that.

  • **Headers and bodies** are only recorded with *Include headers and bodies* on, which is off by

    default. Even then:

    • Authorization, Cookie, Set-Cookie and API-key headers are always redacted;
    • bodies are kept only for text, JSON and form data, up to 10 KB each;
    • JSON fields with sensitive names are masked;
    • WebSocket frame contents are kept only for text frames, up to 2 KB, with the same JSON masking;

      binary frames show only their size.

  • Developers can redact or drop any request with

    sessionReplay: { maskNetworkRequest: (req) => req | null }, or switch network recording off with sessionReplay: { recordNetwork: false }.

  • Admins can delete a single recording; deleting a user's data removes their recordings too.

Page analytics

MetricDefinition
Page viewspage_view events (one on load, one on every SPA navigation)
Visitorsdistinct users (endUserId, else anonymousId) with a page view
Sessionsdistinct SDK session ids with a page view
Avg time on pagemean durationMs of page_leave: visible time only, paused while the tab is hidden
Scroll depthmean of the deepest point reached, 0–100% of the page height
Entries / exitssessions whose first / last page view was the path
Bounce ratesessions with exactly one page view ÷ sessions with a page view

Group dynamic segments folds numeric, UUID and hash-like path segments into :id, so /contacts/42 and /contacts/77 appear as /contacts/:id. Open a page to see its trend, most clicked elements (with rage clicks), the errors that happen on it, and its replays.

Paths come from the event URL. With Collect page URLs off, the server stores no paths, titles, referrers or link targets, and page analytics stay empty.

Autocaptured events

EventProperties
page_viewpath (pathname, no query), title, referrer (first page of a session, other origins only, origin + path), deviceType, browser, os, screenWidth, viewportWidth
page_leavepath, durationMs, scrollDepth
clicktag, text (≤100 chars, never from inputs or masked elements), selector, href (links), x, y
rage_clickas click, plus count: 3+ clicks within 1 s within 30 px, on any element (one event per burst)
exceptiontype, message, frames[] (fn, file, line, col), source, handled, fingerprint (set by the server)
heatmappath, viewportWidth, viewportHeight, pageHeight, points[] (k click/rage/move, x, y, s selector, rx, ry, w weight of a movement bin)

page_leave is sent when the URL changes and when the page is hidden, at most once per page. scrollDepth is 100 for a page that fits in the viewport. Page views wait for consent like other events; clicks, page leave, errors and recordings are only captured once consent is granted.

Clicks are recorded for links, buttons, form controls, [role=button|link|tab|menuitem|…] and anything marked data-guidely-track. Add data-guidely-ignore to an element to skip it and its children. Clicks inside Guidely's own UI are never autocaptured.

Heatmaps

  • **Capture.** With *Heatmaps* on (Settings → Privacy & data → Autocapture, on by default), the SDK

    records every click on a page, not only clicks on buttons and links. Clicks are sent in batches as heatmap events: up to 40 points per batch, and a batch is also sent when the page changes or is hidden. Each point stores page coordinates and the selector of the element it landed on (the surrounding button or link for clicks on an icon), plus its relative position inside that element. Points therefore stay on the right element even when the layout differs between visitors. No text or content is captured. Clicks that are part of a rage-click burst are marked.

  • **Mouse movement** (*Mouse movement*, on by default, desktop pointers only) samples the pointer at

    most every 200 ms while it moves. Samples are binned in the browser, against the element under the pointer (at a 10% grid of its size) or else a 16 px page grid, and only the weighted bins are sent with the click batches. A minute of movement adds a few dozen points, not thousands, and movement never keeps a session alive.

  • **Viewing.** Pages → open a page → **Heatmap**. Choose desktop (≥1280 px), tablet (768–1279 px) or

    mobile (<768 px). Clicks shows a heat layer plus the most-clicked elements (hover one to highlight it on the page); Movement shows where the pointer lingers and the most-hovered elements; Scroll shows how many visitors reached each depth, based on the page_leave scroll depth.

  • **The page behind the heatmap** comes from a session recording that visited the page in that

    viewport size. The recording is replayed to just after the page view and paused. Without recordings the points are drawn on a blank canvas of the page's size.

  • Heatmap points follow the event retention, and with Collect page URLs off they carry no path.

Error tracking

  • The SDK listens for `error` and `unhandledrejection` on `window`. Resource-load errors and opaque

    cross-origin Script error. messages are ignored. Report handled errors yourself with guidely.captureException(error, { area: "checkout" }). With error tracking off for the project nothing is stored, manual reports included; the SDK option captureErrors: false only stops the automatic handlers.

  • **Flood control**: the same error is reported at most 5 times per page load, and at most 100

    errors per session. Errors thrown by Guidely's own SDK are not reported as yours.

  • **Grouping.** The server fingerprints each exception by its type, its message with volatile parts

    (numbers, quoted strings, UUIDs) replaced, and the innermost stack frame's file (build hash removed) and function. Line numbers are left out so an issue survives redeploys.

  • **Issues** are **open**, **resolved** or **ignored**. A resolved issue that happens again reopens

    and is marked Regressed. Ignored issues stay ignored but keep counting.

  • An issue shows its occurrence trend, the latest stack trace, affected users, the pages and

    browsers it happens on, and Watch replay, which opens the recording a few seconds before the error.

Source maps

Upload your build's source maps and the Errors page shows original file names, lines, function names and the surrounding source instead of minified code.

  1. **Create an upload key** in Settings → Source maps (admins). Keys look like `gly_sk_…`, are

    shown once, are stored hashed, and can only upload maps (sourcemaps:write). Unlike the public pk_ keys, they're secrets: keep them in your CI secret store.

  2. Build with source maps (Vite build.sourcemap: true; Next.js productionBrowserSourceMaps: true).
  3. **Upload in CI**, after the build and before deploying:

    ``bash npx @guidelytech/sourcemaps upload --dir dist --url-prefix / --release "$GIT_SHA" --delete-after # Next.js: --dir .next/static --url-prefix /_next/static/ # env: GUIDELY_SOURCEMAP_KEY=gly_sk_… GUIDELY_API_URL=https://<your Guidely API> ``

    The CLI finds every *.map next to its minified file, works out the URL path the file is served at, and uploads in batches. --delete-after removes the local maps so they aren't deployed publicly; --dry-run shows what would be uploaded.

  4. **Tell the SDK the release** (optional but recommended): `createGuidely({ …, release: "" })`.

    With a release, only that release's maps are used. Without one, the most recent map uploaded for the file's path is used, which is safe when file names contain content hashes (Vite, Next.js).

How it's used:

  • **Grouping.** When a map resolves an error's innermost frame, the issue fingerprint uses the

    original file and function name. Issues then keep grouping across deploys even when the minifier renames functions. Occurrences recorded before the maps were uploaded keep their old issue.

  • **Deterministic grouping.** If a map exists but storage can't deliver it in time, the exception

    isn't grouped by its minified frame. The SDK is asked to retry the batch (its other events are stored right away), so one error never splits into two issues because of a slow download. A map whose object is missing or can't be parsed is treated as no map.

  • **Display.** Stack traces are mapped when you open an issue, so older occurrences benefit too.

    Vendor frames are collapsed, and Minified shows the raw frames.

  • **Storage.** Maps are stored gzip-compressed in the same S3 bucket as recordings, under

    sourcemaps/, and are never served to browsers. Guidely never downloads maps from your site. Maps are deleted 180 days after upload; upload them again with each build.

  • Limits. 25 MB per map and 200 maps per request (the CLI batches for you).

Alerts (email and Slack)

Errors → Alerts (editors and above). A rule decides when to notify, and who:

  • **Triggers:**
    • New issue: a fingerprint seen for the first time.
    • Regression: a resolved issue that occurs again.
    • Spike: one issue occurs at least N times within M minutes; the worker checks every minute.
  • Environments: Production only by default.
  • **Cooldown:** at most one alert per issue and rule within the cooldown (default 1 hour), so a

    noisy error can't flood a channel. Ignored issues never alert.

  • **Channels:** any number of email addresses (sent through the notification outbox, so `SMTP_URL`

    must be set; see environment.md), and a Slack channel.

Slack. Integrations → Slack → paste an incoming webhook URL. Create it in Slack under Apps → Incoming Webhooks and pick the channel. Each alert is a message with the error, culprit, occurrences and affected users, plus View issue and Watch replay buttons. Only https://hooks.slack.com/services/… URLs are accepted. Webhook URLs are secrets: the dashboard and API only ever show them masked.

Every alert is logged with its email and Slack delivery status (failed Slack posts are retried up to 5 times). Send test alert checks a rule's channels without waiting for a real error.

Insights

Funnels. Pick 2–8 steps. A step is an event (any built-in or custom event, or any event) with optional filters: a path pattern (/contacts/*), click text, or a guide. A user converts when they complete the steps in order, each within the conversion window (1 hour to 90 days) of their first step-1 event in the date range. Results show users per step, conversion from the previous and the first step, drop-off, and median time between steps. Users are identified people where known, otherwise anonymous visitors.

Retention. Users enter the cohort of the day or week in which they first did the start event in the range. A cohort's columns show the share who did the return event in each following period. Periods that haven't finished yet are left blank, and the average row is weighted by cohort size.

Save an insight to reuse it; editors can rename and delete saved insights.

Data, storage and retention

TableHolds
ProductEventAutocaptured events alongside guide and custom events. New path column for page grouping, indexed by (projectId, sessionId) for replay lookups
ErrorIssueOne row per (project, environment, fingerprint): status, counts, first/last seen. Occurrences are the exception events
ReplaySessionOne per recorded SDK session: user, time span, URLs, device, sizes
ReplayConsoleLogSearchable console output per recording (distinct lines with counts; trigram index)
ReplayChunkIndex of the uploads that make up a recording (S3 key, time span, size), idempotent per (session, page load, sequence)
InsightSaved funnel and retention definitions
ProjectApiKey, SourceMapSecret upload keys (hashed), and the index of uploaded source maps (bytes in S3)
ReplayAlertRule, ReplayAlertReplay alert rules (conditions, channels, hourly limit) and each fired alert (one per rule and recording)
ErrorAlertRule, ErrorAlertAlert rules, and each fired alert (cooldown record, email link, Slack delivery job)
  • Recordings are deleted by the worker after the project's replay retention (default 30 days).

    Events, including exceptions, follow the event retention. Issues that haven't occurred within the event retention are deleted.

  • **Recordings are stored in S3**, one gzip-compressed JSON object per upload at

    replays/<projectId>/<recordingId>/<pageLoadId>/<seq>.json.gz (Postgres holds only the index). Typical size is 50–300 KB per minute of activity. The bucket is private and encrypted. The dashboard never links to S3: recordings are streamed through the authenticated API, so access follows workspace roles. Configure it with REPLAY_S3_* (environment.md). The AWS stack creates the bucket (aws-deployment.md); locally pnpm db:up starts S3-compatible SeaweedFS. Without a bucket, session replay is unavailable and SDKs don't record.

  • Deleting a recording, a user's data or a project deletes the S3 objects too. A bucket lifecycle

    rule expires anything older than 366 days as a backstop.

  • Limits: 2 MB compressed / 16 MB uncompressed per replay upload; the usual SDK rate limits.

    An upload whose S3 write fails is rolled back and answered with 503, so the SDK retries it.

API

SDK: POST /api/v1/sdk/replay (see api.md). Management (session cookie): GET /projects/:id/replays, GET|DELETE /replays/:id, GET /replays/:id/recording, GET /projects/:id/pages, GET /projects/:id/pages/detail, GET /projects/:id/heatmaps, GET /projects/:id/errors, GET|PATCH /errors/:id, POST /projects/:id/insights/funnel|retention, GET|POST /projects/:id/insights, PATCH|DELETE /insights/:id, GET|POST /projects/:id/alert-rules, PATCH|DELETE /alert-rules/:id, POST /alert-rules/:id/test, GET /projects/:id/alert-rules/deliveries, GET|POST /projects/:id/api-keys, DELETE /api-keys/:id, GET|DELETE /projects/:id/sourcemaps, DELETE /sourcemaps/:id, GET|POST /projects/:id/replay-alert-rules, PATCH|DELETE /replay-alert-rules/:id, POST /replay-alert-rules/:id/test, GET /projects/:id/replay-alert-rules/deliveries. Upload key: POST /api/v1/sourcemaps/upload, GET /api/v1/sourcemaps/whoami.

Not yet included

Fetching source maps from your site automatically, Server-Sent Events / WebRTC recording in replays, and trend charts over arbitrary events.