hintuitiondocs

Install the snippet

One <script> tag puts your published experiences inside your own product. No build step, no npm package, nothing to redeploy when you publish something new.

The dashboard's Install page has this snippet with your workspace already filled in — copy it from there rather than editing the example below.

<script
  async
  src="https://hintuition.frugalsaas.com/hintuition-widget.js"
  data-workspace="your-slug"
></script>

Paste it into your app's HTML, ideally just before the closing </body> tag.

async matters: it lets the browser carry on rendering your page while the script loads, so hintuition can never be the reason your app feels slow. Keep it.

Identifying the current user

Optional, and it's what makes trait targeting work. Call identify() with your own user ID and whatever you want to target on:

window.hintuition.identify("user-123", { plan: "pro", role: "admin" });

Call it as soon as you know who's signed in — after that, changing user re-checks straight away, so signing in shows the signed-in person's experiences without a reload.

If you already know the user when the page is built, set it before the script tag instead, and the very first page view is already targeted:

<script>
  window.hintuition = {
    identify: "user-123",
    traits: { plan: "pro", role: "admin" },
  };
</script>

Send only what you target on. Traits travel in the query string of a normal GET request, which means they land in access logs the same way any URL does. A plan name or a role is fine. Email addresses, names and anything you'd call personal data are not — and we never store traits, so nothing is gained by sending them.

People you haven't identified

Someone who isn't signed in still sees experiences that have no trait rules on them. To remember what they've already been shown, the widget stores one random ID in localStorage on your domain. It holds no personal data, it's never shared between customers, and it's how "don't show this again" survives a reload.

If storage is blocked — private browsing, a strict cookie policy — the widget still shows experiences, but can't remember doing so. That visitor may see the same message again on their next page view. We'd rather that than invent an identifier that follows someone around.

Single-page apps

Nothing extra to do. The widget follows client-side navigation (pushState, replaceState and the back button) and re-checks on each route change, so an experience scoped to /settings appears when someone navigates there, not only when they hard-load the page.

Content Security Policy

The snippet needs no script-src concessions: it's an external file and it never evaluates strings, so allowlisting the hintuition origin is enough.

It injects one <style> element. If your style-src doesn't allow inline styles, pass your nonce and we'll put it on that element:

<script
  async
  src="https://hintuition.frugalsaas.com/hintuition-widget.js"
  data-workspace="your-slug"
  data-nonce="YOUR_REQUEST_NONCE"
></script>

That's the whole story, for every surface. You will never be asked to add 'unsafe-inline' for hintuition — tooltips and hotspots included, now that they're live.

That last part is worth stating plainly, because most of this category says otherwise. Anchored tooltips are positioned by writing computed coordinates onto an element, and the common claim — in competitors' own CSP documentation — is that this forces you to weaken style-src. It doesn't: a Content Security Policy governs the style attribute and <style> elements, not property writes made through the CSSOM. We verified that in Chrome, Firefox and Safari under a policy with no unsafe-inline — all three behave identically — and the test lives in our suite so that if it ever stops being true we find out, rather than your security review does.

An earlier version of this page said the opposite — that tooltips would one day need the concession. It was wrong, and it is withdrawn rather than quietly deleted, because anyone who already planned around it deserves to know.

Builder mode, and why it can't be turned on from outside

Your team can point at an element by clicking it in your own running app. That means the snippet you installed can, in one specific case, load a second file that draws a picking overlay. Since that is authoring code running in your product, here is exactly what gates it.

You list the addresses where it's allowed. Builder mode does nothing on an origin your workspace hasn't added. A workspace with an empty list cannot use it at all — which is also how you switch it off permanently: leave the list empty.

A pass is minted per session, for one workspace and one origin, and expires in minutes. It is single-use, and reuse is refused server-side.

The snippet never decides whether a pass is valid. It asks our API, which checks the signature, the expiry, whether the pass has already been used, and whether the request is coming from an address you allowed. Anything short of all four and nothing loads — no overlay, no second file, no console noise.

Your end users never download any of it. The picking code is a separate file requested only after that check passes. An ordinary visitor never asks for it.

The pass travels in the URL fragment, not the query string, so it never reaches your access logs, your analytics or a referrer header — and the snippet removes it from the address bar the moment it boots.

When something goes wrong

The widget is built to fail invisibly. If our API is unreachable, slow, or returns something it doesn't understand, it renders nothing and adds no errors to your console — your app carries on exactly as if it weren't installed. It reports the failure to us instead, as a fixed error code with no page data in it.

The practical consequence: delivery is best-effort. Never make an experience the only channel for something that legally or operationally has to reach someone — a billing failure, a security notice, a breaking API change. Email those. Use hintuition to make sure people also see them in context.