Skip to main content

Hotjar vs FullStory vs Microsoft Clarity: Session Replay and Heatmaps (2026)

·PkgPulse Team

TL;DR

Hotjar is the product experience insights platform — session recordings, heatmaps, feedback widgets, surveys, funnels, used by product teams to understand user behavior. FullStory is the digital experience intelligence platform — session replay, frustration signals, product analytics, error tracking, enterprise-grade search and segmentation. Microsoft Clarity is the free session replay tool — heatmaps, session recordings, rage click detection, scroll maps, completely free with no traffic limits. In 2026: Hotjar for product teams wanting insights + feedback, FullStory for enterprise digital experience analytics, Clarity for free session replay at any scale.

Key Takeaways

  • Hotjar: Session replay + heatmaps + surveys + feedback widgets
  • FullStory: Session replay + product analytics + frustration detection + error correlation
  • Microsoft Clarity: Free session replay + heatmaps + rage click detection, no traffic limits
  • Hotjar has the best user feedback tools (surveys, polls, widgets)
  • FullStory has the most powerful search and segmentation
  • Clarity is 100% free with unlimited sessions

Hotjar

Hotjar — product experience insights:

Setup

<!-- Hotjar tracking code: -->
<script>
  (function(h,o,t,j,a,r){
    h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
    h._hjSettings={hjid:YOUR_SITE_ID,hjsv:6};
    a=o.getElementsByTagName('head')[0];
    r=o.createElement('script');r.async=1;
    r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
    a.appendChild(r);
  })(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
</script>

Next.js integration

// app/components/HotjarAnalytics.tsx
"use client"

import { useEffect } from "react"
import { usePathname } from "next/navigation"

declare global {
  interface Window {
    hj: (...args: any[]) => void
  }
}

export function HotjarAnalytics({ siteId }: { siteId: number }) {
  useEffect(() => {
    // Initialize Hotjar:
    (function(h: any, o: any, t: string, j: string, a?: any, r?: any) {
      h.hj = h.hj || function() { (h.hj.q = h.hj.q || []).push(arguments) }
      h._hjSettings = { hjid: siteId, hjsv: 6 }
      a = o.getElementsByTagName("head")[0]
      r = o.createElement("script")
      r.async = 1
      r.src = t + h._hjSettings.hjid + j + h._hjSettings.hjsv
      a.appendChild(r)
    })(window, document, "https://static.hotjar.com/c/hotjar-", ".js?sv=")
  }, [siteId])

  return null
}

// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <HotjarAnalytics siteId={Number(process.env.NEXT_PUBLIC_HOTJAR_ID)} />
        {children}
      </body>
    </html>
  )
}

Events, user identification, and surveys

// Identify user:
window.hj("identify", "user-123", {
  email: "royce@example.com",
  plan: "pro",
  signup_date: "2026-01-15",
})

// Track custom events:
window.hj("event", "package_compared")
window.hj("event", "report_downloaded")
window.hj("event", "subscription_started")

// Trigger survey programmatically:
window.hj("trigger", "comparison-feedback")

// Tag recordings:
window.hj("tagRecording", ["pro-user", "comparison-page"])

// State change (for SPAs):
window.hj("stateChange", "/packages/react")

// React hook:
function useHotjar() {
  return {
    identify: (userId: string, attributes: Record<string, any>) => {
      if (typeof window !== "undefined" && window.hj) {
        window.hj("identify", userId, attributes)
      }
    },
    event: (eventName: string) => {
      if (typeof window !== "undefined" && window.hj) {
        window.hj("event", eventName)
      }
    },
    trigger: (surveyId: string) => {
      if (typeof window !== "undefined" && window.hj) {
        window.hj("trigger", surveyId)
      }
    },
  }
}

function ComparisonPage() {
  const hotjar = useHotjar()

  useEffect(() => {
    hotjar.event("comparison_page_viewed")
  }, [])

  return (
    <div>
      <ComparisonTable />
      <button onClick={() => hotjar.trigger("comparison-feedback")}>
        Give Feedback
      </button>
    </div>
  )
}

Feedback widgets

// Hotjar Feedback widget (configured in dashboard):
// - Incoming Feedback: always-visible widget for user comments
// - Surveys: multi-step questionnaires
// - User Interviews: recruit users for research

// Trigger specific feedback:
window.hj("trigger", "nps-survey")      // Net Promoter Score
window.hj("trigger", "exit-intent")      // When user leaves
window.hj("trigger", "post-purchase")    // After conversion

// Control recording:
window.hj("trigger", "start_recording")  // Force start
// Session recordings auto-capture:
// - Rage clicks
// - U-turns (back-and-forth navigation)
// - Error clicks

FullStory

FullStory — digital experience intelligence:

Setup

// Using @fullstory/browser:
import * as FullStory from "@fullstory/browser"

FullStory.init({
  orgId: process.env.NEXT_PUBLIC_FULLSTORY_ORG_ID!,
  devMode: process.env.NODE_ENV === "development",
})

Next.js integration

// app/components/FullStoryAnalytics.tsx
"use client"

import { useEffect } from "react"
import * as FullStory from "@fullstory/browser"

export function FullStoryAnalytics() {
  useEffect(() => {
    FullStory.init({
      orgId: process.env.NEXT_PUBLIC_FULLSTORY_ORG_ID!,
      devMode: process.env.NODE_ENV === "development",
    })
  }, [])

  return null
}

// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <FullStoryAnalytics />
        {children}
      </body>
    </html>
  )
}

User identification and events

import * as FullStory from "@fullstory/browser"

// Identify user:
FullStory.identify("user-123", {
  displayName: "Royce",
  email: "royce@example.com",
  plan_str: "pro",
  signupDate_date: new Date("2026-01-15"),
  packagesTracked_int: 25,
})

// Track custom events:
FullStory.event("Package Compared", {
  packages_strs: ["react", "vue"],
  comparisonType_str: "downloads",
  resultCount_int: 2,
})

FullStory.event("Report Downloaded", {
  format_str: "pdf",
  packageCount_int: 10,
})

FullStory.event("Subscription Started", {
  plan_str: "pro",
  price_real: 29.99,
  billingCycle_str: "monthly",
})

// Set page properties:
FullStory.setProperties("page", {
  pageName_str: "Package Comparison",
  category_str: "analytics",
})

// Consent management:
FullStory.consent(true)   // User consented
FullStory.consent(false)  // Revoke consent
FullStory.restart()       // Restart after consent

// Get session URL (for support tickets):
const sessionUrl = FullStory.getCurrentSessionURL(true)  // true = with timestamp
console.log(`Support replay: ${sessionUrl}`)
import * as FullStory from "@fullstory/browser"

// FullStory automatically detects:
// - Rage Clicks: rapid clicking on same element
// - Dead Clicks: clicks that produce no response
// - Error Clicks: clicks that trigger JS errors
// - Thrashed Cursor: rapid mouse movement (confusion)

// React error boundary integration:
class FullStoryErrorBoundary extends React.Component {
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // FullStory automatically captures errors,
    // but you can add custom context:
    FullStory.event("React Error Boundary", {
      error_str: error.message,
      component_str: errorInfo.componentStack || "unknown",
    })
  }

  render() {
    return this.props.children
  }
}

// Search API (server-side):
// FullStory provides powerful search:
// - Find sessions with rage clicks on checkout button
// - Find sessions where users hit errors after comparing packages
// - Find sessions by user attributes (plan, signup date)
// - Combine with custom events for precise segment analysis

// Session replay link for customer support:
function SupportWidget() {
  const getReplayLink = () => {
    const url = FullStory.getCurrentSessionURL(true)
    navigator.clipboard.writeText(url || "")
  }

  return (
    <button onClick={getReplayLink}>
      Copy Session Link for Support
    </button>
  )
}

Microsoft Clarity

Microsoft Clarity — free session replay:

Setup

<!-- Clarity tracking code: -->
<script type="text/javascript">
  (function(c,l,a,r,i,t,y){
    c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
    t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
    y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
  })(window, document, "clarity", "script", "YOUR_PROJECT_ID");
</script>

Next.js integration

// app/components/ClarityAnalytics.tsx
"use client"

import { useEffect } from "react"

declare global {
  interface Window {
    clarity: (...args: any[]) => void
  }
}

export function ClarityAnalytics({ projectId }: { projectId: string }) {
  useEffect(() => {
    (function(c: any, l: any, a: string, r: string, i: string, t?: any, y?: any) {
      c[a] = c[a] || function() { (c[a].q = c[a].q || []).push(arguments) }
      t = l.createElement(r)
      t.async = 1
      t.src = "https://www.clarity.ms/tag/" + i
      y = l.getElementsByTagName(r)[0]
      y.parentNode.insertBefore(t, y)
    })(window, document, "clarity", "script", projectId)
  }, [projectId])

  return null
}

// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ClarityAnalytics projectId={process.env.NEXT_PUBLIC_CLARITY_PROJECT_ID!} />
        {children}
      </body>
    </html>
  )
}

Custom tags and events

// Identify user:
window.clarity("identify", "user-123", "session-456", "comparison-page")

// Custom tags (filter recordings by tag):
window.clarity("set", "plan", "pro")
window.clarity("set", "page_type", "comparison")
window.clarity("set", "user_segment", "power_user")

// Custom events:
window.clarity("event", "Package Compared")
window.clarity("event", "Report Downloaded")
window.clarity("event", "Subscription Started")

// Consent management:
window.clarity("consent")  // Mark session as consented

// Upgrade session (ensure this session is recorded):
window.clarity("upgrade", "high-value-session")

// React hook:
function useClarity() {
  return {
    identify: (userId: string, sessionId?: string) => {
      if (typeof window !== "undefined" && window.clarity) {
        window.clarity("identify", userId, sessionId)
      }
    },
    set: (key: string, value: string) => {
      if (typeof window !== "undefined" && window.clarity) {
        window.clarity("set", key, value)
      }
    },
    event: (name: string) => {
      if (typeof window !== "undefined" && window.clarity) {
        window.clarity("event", name)
      }
    },
    upgrade: (reason: string) => {
      if (typeof window !== "undefined" && window.clarity) {
        window.clarity("upgrade", reason)
      }
    },
  }
}

function ComparisonPage({ userId }: { userId: string }) {
  const clarity = useClarity()

  useEffect(() => {
    clarity.identify(userId)
    clarity.set("page_type", "comparison")
  }, [userId])

  const handleCompare = () => {
    clarity.event("compare_clicked")
    clarity.upgrade("conversion-event")  // Ensure this session is saved
  }

  return <button onClick={handleCompare}>Compare</button>
}

Clarity features (auto-detected)

// Microsoft Clarity automatically detects and surfaces:

// 1. Rage Clicks — rapid repeated clicks on the same area
//    → Indicates frustration with unresponsive UI elements

// 2. Dead Clicks — clicks that produce no page change or response
//    → Indicates confusing UX where users expect interactivity

// 3. Quick Backs — users navigate to a page then immediately return
//    → Indicates misleading links or wrong content

// 4. Excessive Scrolling — scrolling significantly past main content
//    → Indicates users can't find what they're looking for

// 5. Scroll Depth — how far users scroll on each page
//    → Heatmap shows where attention drops off

// Clarity Dashboard provides:
// - Session recordings (filtered by frustration signals)
// - Click heatmaps
// - Scroll heatmaps
// - Insights dashboard (automatic AI insights)
// - Google Analytics integration
// - Copilot AI summaries of session patterns

// No API access — dashboard-only analytics
// No traffic limits — records all sessions for free
// GDPR compliant — no personal data collection by default

Feature Comparison

FeatureHotjarFullStoryMicrosoft Clarity
Session replay
Heatmaps (click)
Heatmaps (scroll)
Rage click detection
Dead click detection
Frustration scoring✅ (basic)✅ (advanced)✅ (basic)
Custom events✅ (typed)✅ (tags)
User identification
Surveys/feedback✅ (built-in)
Product analytics✅ (funnels)✅ (advanced)
Error correlation
Search & segmentation✅ (basic)✅ (powerful)✅ (basic)
API access
AI insights✅ (Copilot)
GA integration
Data export
Free tier35 daily sessions1K sessions/mo✅ (unlimited)
PricingFrom $39/monthCustom pricingFree

When to Use Each

Use Hotjar if:

  • Want session replay combined with user feedback tools
  • Need surveys, feedback widgets, and NPS built-in
  • Building product insights for product/UX teams
  • Want heatmaps + recordings + feedback in one tool

Use FullStory if:

  • Need enterprise-grade digital experience analytics
  • Want powerful search and segmentation across sessions
  • Need error correlation with session replays
  • Building data-driven product teams with advanced analytics

Use Microsoft Clarity if:

  • Want completely free session replay with no limits
  • Need basic heatmaps and rage/dead click detection
  • Starting with UX analytics and don't need advanced features
  • Want AI-powered session summaries (Copilot)

Methodology

Feature comparison based on Hotjar, FullStory, and Microsoft Clarity platforms and pricing as of March 2026.

Compare analytics and developer tooling on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.