Skip to main content

Tour Kit + Sentry: catch tour errors before users report them

Wire Tour Kit callbacks to Sentry breadcrumbs and error boundaries so tour failures surface in your dashboard, not in support tickets. TypeScript examples.

DomiDex
DomiDexCreator of Tour Kit
April 9, 20267 min read
Share
Tour Kit + Sentry: catch tour errors before users report them

Tour Kit + Sentry: catch tour errors before users report them

Product tours interact with the DOM in brittle ways. They query selectors, calculate positions relative to moving targets, and overlay UI on top of content that might not exist yet. When a tour breaks, the failure is usually silent: the tooltip doesn't appear, the highlight renders in the wrong spot, or the whole tour quietly fails to start. Users don't file bug reports for "the onboarding thing didn't show up." They just leave.

Sentry captures these failures with enough context to reproduce them. As of April 2026, @sentry/react v7 ships at 52.67KB minified (a 29% reduction from v6, per Sentry's engineering blog), and under 20KB gzipped with tree-shaking. The React SDK includes an error boundary component, a performance profiler, and hooks for adding breadcrumbs and tags to error events. That's exactly what Tour Kit's lifecycle callbacks map to.

This tutorial wires Tour Kit's onTourStart, onStepView, onTourComplete, and onTourSkip callbacks to Sentry breadcrumbs, wraps tours in a Sentry error boundary with tour-specific context, and sets up an alert rule so tour failures ping your team before users notice.

npm install @tourkit/core @tourkit/react @sentry/react

What you'll build

You'll connect Tour Kit's four lifecycle callbacks to Sentry breadcrumbs so every error report includes a trail showing which tour was active, which step the user was on, and what happened before the crash. You'll also wrap your tour components in Sentry's ErrorBoundary with tour-specific tags, giving you a filtered Sentry view of tour-only errors. The whole integration is about 60 lines of TypeScript across two files.

One limitation upfront: Tour Kit requires React 18+ and doesn't have a built-in Sentry adapter. You'll wire callbacks manually. And Tour Kit is our project, so take the integration claims with appropriate skepticism. Every code example here is verifiable against the Sentry and Tour Kit docs.

Why Sentry + Tour Kit?

Tour errors are a specific category of UI failure that traditional error monitoring misses. Research shows 78% of users abandon product tours by step 3, and 76.3% dismiss static tooltips within 3 seconds. When tours break on top of that, you're losing users to bugs you can't even see. A document.querySelector returning null because the target element hasn't mounted yet isn't a thrown exception. It's a silent failure that causes a cascade of positioning bugs. Sentry's breadcrumb system captures these non-exception events as part of the error timeline, so when a real exception does fire (like a TypeError: Cannot read properties of null), you can see that it happened right after step 3 of the "dashboard-intro" tour tried to highlight a lazy-loaded chart component.

The alternative is console.log debugging in production. According to Sentry's 2024 developer survey, teams using structured error monitoring resolve frontend issues 2.3x faster than those relying on user reports and log grepping. And as the LogRocket blog documents, React error boundaries are the only reliable way to catch render-phase errors in production without crashing the entire component tree.

Tour Kit's callback architecture makes this integration clean. Every tour lifecycle event (start, step view, complete, skip) fires a typed callback with the tour ID, step ID, and step index. These map directly to Sentry breadcrumbs without any adapter layer.

Prerequisites

  • React 18.2+ or React 19
  • A Sentry account (the free tier covers 5,000 errors/month, as of April 2026)
  • Tour Kit installed (@tourkit/core + @tourkit/react)
  • A working product tour with at least 2 steps

No tour yet? The Next.js App Router tutorial gets you there in under 5 minutes.

Step 1: Initialize Sentry in your React app

Sentry's React SDK wraps the core @sentry/browser package with React-specific integrations: an error boundary component, a profiler, and React Router instrumentation. Initialize it once at the root of your app.

// src/lib/sentry.ts
import * as Sentry from '@sentry/react'

export function initSentry() {
  Sentry.init({
    dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
    environment: process.env.NODE_ENV,
    tracesSampleRate: 0.2,
    beforeBreadcrumb(breadcrumb) {
      // Keep tour breadcrumbs even when trimming others
      if (breadcrumb.category === 'tour') return breadcrumb
      return breadcrumb
    },
  })
}

Call initSentry() from your root layout or entry point before any React rendering. The beforeBreadcrumb callback ensures tour-related breadcrumbs survive Sentry's default breadcrumb limit (100 per event).

// src/app/layout.tsx (Next.js App Router)
'use client'

import { useEffect } from 'react'
import { initSentry } from '@/lib/sentry'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    initSentry()
  }, [])

  return <html lang="en"><body>{children}</body></html>
}

Step 2: Connect Tour Kit callbacks as Sentry breadcrumbs

Tour Kit's TourKitProvider (or the standalone Tour component) accepts lifecycle callbacks. Each callback adds a Sentry breadcrumb with structured data, building a timeline of exactly what the user did in the tour before things went wrong.

// src/lib/tour-sentry.ts
import * as Sentry from '@sentry/react'

export const tourSentryCallbacks = {
  onTourStart: (tourId: string) => {
    Sentry.addBreadcrumb({
      category: 'tour',
      message: `Tour started: ${tourId}`,
      level: 'info',
      data: { tourId },
    })

    // Tag the current scope so all errors during this tour
    // are filterable by tour ID in the Sentry dashboard
    Sentry.setTag('active_tour', tourId)
  },

  onStepView: (tourId: string, stepId: string, stepIndex: number) => {
    Sentry.addBreadcrumb({
      category: 'tour',
      message: `Step viewed: ${stepId} (${stepIndex})`,
      level: 'info',
      data: { tourId, stepId, stepIndex },
    })
    Sentry.setTag('tour_step', stepId)
  },

  onTourComplete: (tourId: string) => {
    Sentry.addBreadcrumb({
      category: 'tour',
      message: `Tour completed: ${tourId}`,
      level: 'info',
      data: { tourId },
    })
    Sentry.setTag('active_tour', '')
  },

  onTourSkip: (tourId: string, stepIndex: number) => {
    Sentry.addBreadcrumb({
      category: 'tour',
      message: `Tour skipped at step ${stepIndex}: ${tourId}`,
      level: 'warning',
      data: { tourId, stepIndex },
    })
    Sentry.setTag('active_tour', '')
  },
}

Wire these callbacks into your provider:

// src/app/providers.tsx
'use client'

import { TourKitProvider } from '@tourkit/react'
import { tourSentryCallbacks } from '@/lib/tour-sentry'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <TourKitProvider
      onTourStart={tourSentryCallbacks.onTourStart}
      onStepView={tourSentryCallbacks.onStepView}
      onTourComplete={tourSentryCallbacks.onTourComplete}
      onTourSkip={tourSentryCallbacks.onTourSkip}
    >
      {children}
    </TourKitProvider>
  )
}

Now every Sentry error report that happens during an active tour will include the breadcrumb trail showing which tour, which step, and whether the user was mid-tour or had just completed/skipped.

Step 3: Wrap tours in a Sentry error boundary

Breadcrumbs capture context for errors that bubble up naturally. But some tour failures don't throw. They result in a broken render that React's error boundary catches. As Smashing Magazine covers in depth, combining Sentry with React error boundaries gives you both the capture and the graceful fallback in one component.

// src/components/tour-error-boundary.tsx
'use client'

import * as Sentry from '@sentry/react'

interface TourErrorBoundaryProps {
  tourId: string
  children: React.ReactNode
}

export function TourErrorBoundary({ tourId, children }: TourErrorBoundaryProps) {
  return (
    <Sentry.ErrorBoundary
      beforeCapture={(scope) => {
        scope.setTag('component', 'product-tour')
        scope.setTag('tour_id', tourId)
        scope.setLevel('error')
      }}
      fallback={({ resetError }) => (
        <div role="alert">
          <p>The tour ran into a problem. You can continue using the app normally.</p>
          <button type="button" onClick={resetError}>Dismiss</button>
        </div>
      )}
    >
      {children}
    </Sentry.ErrorBoundary>
  )
}

Wrap individual tours that you suspect might fail, especially tours targeting dynamically loaded content:

// src/components/dashboard-tour.tsx
import { Tour, TourStep } from '@tourkit/react'
import { TourErrorBoundary } from './tour-error-boundary'

export function DashboardTour() {
  return (
    <TourErrorBoundary tourId="dashboard-intro">
      <Tour tourId="dashboard-intro">
        <TourStep target="#revenue-chart" title="Revenue overview">
          This chart updates in real time.
        </TourStep>
        <TourStep target="#user-table" title="Active users">
          Click any row to see details.
        </TourStep>
      </Tour>
    </TourErrorBoundary>
  )
}

The gotcha we hit: if your tour target is inside a Suspense boundary, the element might not exist when the tour tries to highlight it. Sentry captures the resulting error, and the tour_id tag tells you exactly which tour caused it. Without the tag, you'd see a generic "Cannot read properties of null" error with no clue it was tour-related.

Step 4: Verify it works

Trigger a deliberate error to confirm the pipeline works end to end. Point a tour step at a selector that doesn't exist:

<TourStep target="#nonexistent-element" title="This will fail">
  Testing Sentry integration.
</TourStep>

Start the tour, then check your Sentry dashboard. You should see:

  1. An error event with the tag active_tour: dashboard-intro
  2. A breadcrumb trail showing Tour started → Step viewed: step-0 → Step viewed: step-1 before the failure
  3. The component: product-tour tag making the event filterable

Set up a Sentry alert rule for the component:product-tour tag so tour failures notify your team in Slack or email. In the Sentry dashboard, go to Alerts → Create Alert Rule → Issue Alert, set the filter to tags[component]:product-tour, and route it to your frontend channel.

Remove the test step after verifying.

Going further

Three patterns that build on this foundation:

Custom contexts for step metadata. Add Sentry.setContext('tour_state', { tourId, stepId, totalSteps }) in the onStepView callback. This attaches structured data (not just tags) to every error, making it searchable in Sentry's Discover queries.

Performance spans for tour rendering. Wrap tour initialization in Sentry.startSpan({ name: 'tour.render', op: 'ui.react' }) to measure how long tours take to render and highlight. Slow tours (>500ms to first highlight) show up in Sentry's performance dashboard.

The @tourkit/analytics plugin route. If you're already using Tour Kit's analytics package, you can write a Sentry plugin that implements the AnalyticsPlugin interface. This gives you centralized tracking through AnalyticsProvider instead of per-provider callbacks:

// src/lib/sentry-analytics-plugin.ts
import * as Sentry from '@sentry/react'
import type { AnalyticsPlugin, TourEvent } from '@tourkit/analytics'

export const sentryPlugin: AnalyticsPlugin = {
  name: 'sentry',
  track: (event: TourEvent) => {
    Sentry.addBreadcrumb({
      category: 'tour',
      message: `${event.eventName}: ${event.tourId}`,
      level: event.eventName.includes('skip') ? 'warning' : 'info',
      data: {
        tourId: event.tourId,
        stepId: event.stepId,
        stepIndex: event.stepIndex,
      },
    })

    if (event.eventName === 'tour_started') {
      Sentry.setTag('active_tour', event.tourId)
    }
  },
}

FAQ

Does adding Sentry breadcrumbs affect tour performance?

Sentry breadcrumbs are synchronous in-memory operations that take microseconds. addBreadcrumb pushes to an array capped at 100 entries. There's no network call, no serialization cost, and no impact on tour rendering. The overhead is functionally zero.

Yes. The Sentry.setTag('component', 'product-tour') call in the error boundary tags every tour error. In the Sentry Issues view, filter with component:product-tour to see only tour failures. You can also create a saved search and a dedicated alert rule for this tag.

What about errors in tours that don't throw exceptions?

Silent failures (a target element missing, a tooltip rendering off-screen) don't trigger error boundaries because no exception is thrown. For these, add defensive checks in your tour step configuration and call Sentry.captureMessage() when a target selector returns null. Tour Kit's positioning engine handles missing targets gracefully, but you still want visibility into how often it happens.

Does this work with Tour Kit's free tier?

Yes. The lifecycle callbacks (onTourStart, onStepView, onTourComplete, onTourSkip) are part of @tourkit/react, which is MIT-licensed and free. The @tourkit/analytics plugin route is also free. Nothing in this integration requires Tour Kit Pro.

How much does Sentry add to my bundle?

As of April 2026, @sentry/react v7 is 52.67KB minified (down 29% from v6). With tree-shaking, importing only init, addBreadcrumb, setTag, and ErrorBoundary brings the gzipped impact under 20KB. As Shu Ding from Vercel noted, "We were able to reduce [the bundle] further through tree shaking" (Sentry Blog). Sentry also supports lazy loading, which defers the cost until after initial page load. Combined with Tour Kit core (under 8KB gzipped), the entire monitored tour system adds under 28KB.

Ready to try userTourKit?

$ pnpm add @tour-kit/react