SDK reference

The tracker ships as two packages: @wnstn/canopy is the vanilla core — under 2kb, no framework required — and @wnstn/canopy-nextjs wraps it for React with a provider and hooks. Both are tree-shakeable, type-safe, and send events to your own domain with no cookies and no browser storage.

1Installation

terminal
npm i @wnstn/canopy          # vanilla core
npm i @wnstn/canopy-nextjs   # React / Next.js wrapper (includes the core)

Using the script tag instead of bundling? Skip this page's install sections — the quick start covers the tag attributes, and everything below about options and hooks still applies via window.Canopy.init(options).

2Initializing

createCanopy (a factory — no new, no side effects at import time) validates your options, starts the tracker, and returns an instance:

init
import { createCanopy } from "@wnstn/canopy";

const canopy = createCanopy({
  siteId: "canopy_example_com",
  debug: process.env.NODE_ENV === "development",
});

Initialization is idempotent: calling it twice updates the existing instance's options instead of creating a second tracker. If siteId is missing, initialization throws a CanopyError (see error handling).

Options

OptionTypeDefaultDescription
siteIdstringrequiredYour site ID, e.g. canopy_example_com.
endpointstring/api/canopy/eventWhere events are POSTed. Set a full URL for cross-origin installs.
envstring—Label attached to every event, e.g. production or staging.
manualPageviewbooleanfalseSuppress the automatic pageview on load.
disableOutboundbooleanfalseDisable automatic outbound click tracking.
respectDntbooleantrueDrop all events when the browser sends Do Not Track.
sampleRatenumber1Send only a fraction of events, 0–1. Useful for very high-traffic sites.
propsobject—Default properties merged into every event.
debugbooleanfalseLog every send to the console.
hooksobject—beforeSend / afterSend / onError interceptors.

3Instance methods

MethodDescription
track(name, props?)Send a custom event with optional properties.
pageview(props?)Send a pageview for the current URL. Automatic unless manualPageview is set.
identify(props?)Send an identify event with user traits stored in props.
reset()Discard the current session ID — the next event starts a new session.
setDefaultProps(props)Merge properties into the defaults attached to every event.
getSessionId()Return the current in-memory session ID.
optionsRead the current configuration (also assignable to update it).

Typical lifecycle for a logged-in app:

lifecycle
canopy.identify({ userId: user.id, plan: user.plan });  // after login
canopy.track("feature_used", { name: "export" });        // anywhere
canopy.reset();                                          // on logout

4Module-level functions

The package also exports standalone functions that work before initialization by queueing internally. Once createCanopy runs, the queue replays in order — so import order and init order never matter:

pre-init queuejust works
import { track, pageview, identify } from "@wnstn/canopy";

track("signup_started");        // queued
pageview({ path: "/landing" }); // queued
identify({ userId: "123" });    // queued
createCanopy({ siteId: "canopy_example_com" }); // queue replays here
FunctionDescription
track(name, props?)Queue or send a custom event.
pageview(props?)Queue or send a pageview.
identify(props?)Queue or send an identify event.
createCanopy(options)Initialize the tracker (alias: initCanopy).
setDefaultProps(props)Set defaults on the initialized instance.
getSessionId()Current session ID.
flushQueue()Force-replay queued calls (mainly for tests).
isInitialized()Whether the tracker has started.
getOptions()Current options of the initialized instance.
getCanopy()The live instance, or null before init.
CanopyErrorThe error class used for config, network, and validation failures.

5Hooks

Hooks intercept every send — including automatic pageviews and outbound clicks — which makes them the place for redaction, filtering, and logging. All three can be async, and an error thrown inside a hook is caught and logged so it never breaks tracking:

HookSignatureUse
beforeSend(event) => event | falseModify the event, or return false to cancel the send.
afterSend(event, response) => voidRuns after a successful send; response may be null.
onError(event, error) => voidRuns when a send failed.
hooks in practice
createCanopy({
  siteId: "canopy_example_com",
  hooks: {
    beforeSend: (event) => {
      if (event.n === "outbound") return false;      // skip outbound clicks
      if (event.props?.email) event.props.email = "[redacted]";
      return event;                                   // or a modified event
    },
    afterSend: (event) => console.debug("sent", event.n),
    onError: (event, error) => reportToSentry(error),
  },
});

6Automatic behavior

After initialization the tracker quietly does the work you would otherwise wire up by hand:

BehaviorDetails
PageviewsOne on load, then again on every SPA navigation — pushState, replaceState, popstate, and hashchange are all observed.
Outbound clicksA capture-phase click listener emits an outbound event with the link's href and text (up to 80 chars) whenever the target origin differs from yours.
Do Not TrackWhen the browser signal is on, every event is dropped. Opt out of respecting it with respectDnt: false — not recommended.
BotsRequests from user agents matching common bot patterns are dropped.
Local filesPages loaded from file: are never tracked.
SamplingsampleRate: 0.5 sends roughly half of all events, chosen per event.

Transport

Events are sent with navigator.sendBeacon when available so they survive page unload, falling back to fetch with keepalive. Requests are made with credentials: "omit" — nothing identifying rides along, and no response handling delays navigation.

Sessions

A session is a random UUID generated in memory when the first event needs it — never written to a cookie, localStorage, or anywhere else. It survives SPA navigations (the page never reloaded) and disappears when the tab closes, which is why Canopy needs neither a consent banner nor a privacy policy exception. reset() discards it on demand, e.g. on logout.

Debugging

Set debug: true to log each send to the console, and check the network tab for a 202 response from your endpoint.

7Error handling

Failures surface as CanopyError — a standard Error with a machine-readable code and typed helpers, so callers can branch on the kind of failure without string matching:

CanopyError
try {
  createCanopy({}); // missing siteId
} catch (e) {
  if (e instanceof CanopyError && e.isConfigError()) {
    // code: "CONFIG_ERROR"
  }
}
CodeHelperRaised when
CONFIG_ERRORisConfigError()Invalid options — e.g. missing siteId.
NETWORK_ERRORisNetworkError()A send could not reach the endpoint.
VALIDATION_ERRORisValidationError()An event failed validation before sending.
SEND_CANCELLEDisCancelledError()A beforeSend hook cancelled the send.

8Next.js wrapper

@wnstn/canopy-nextjs re-exports the entire core API and adds a provider and hooks. Mount CanopyProvider once in your root layout — it initializes the tracker synchronously on the client and keeps options fresh across renders:

app/layout.tsxapp router
import { CanopyProvider } from "@wnstn/canopy-nextjs";

export default function RootLayout({ children }) {
  return (
    <CanopyProvider siteId={process.env.NEXT_PUBLIC_CANOPY_SITE_ID!}>
      {children}
    </CanopyProvider>
  );
}

The provider accepts every core option plus scriptSrc: set it to a URL to additionally render a next/script tag, or false to suppress the script entirely (the default — the provider initializes the bundled tracker itself, so a script tag is usually unnecessary). CanopyScript is also exported standalone if you want just the tag.

Hooks

HookReturnsNotes
useCanopyTrack()track(name, props?)Stable function; falls back to the queue outside a provider.
useCanopyPageview()pageview(props?)Manual pageview, e.g. for custom routers.
useCanopyIdentify()identify(props)Attach traits after login.
useCanopyReset()reset()Start a fresh session, e.g. on logout.
useCanopyInstance()all methods + optionsFull instance access in one hook.
useCanopy()instance | nullRaw instance, or null before initialization.
useCanopyInitialized()booleanWhether the tracker is live.
useCanopyOptions()options | nullCurrent configuration.
useCanopyFlush()() => voidFlush queued pre-init calls.
server components
The tracker exists only in the browser, so hooks belong in client components. Server components render untouched — wrap only the interactive parts, or drop a 'use client' boundary where you track.

9Event payload

For reference — and for building your own ingestion clients — this is exactly what the tracker POSTs. Field meanings and server-side limits are documented in the HTTP API reference:

POST bodyapplication/json
{
  "v": "0.1.0",                          // SDK version
  "s": "canopy_example_com",             // site ID
  "sid": "3f9c2a1e-...",                 // in-memory session ID
  "n": "pageview",                       // event name
  "u": "https://example.com/pricing",    // full URL
  "r": "https://google.com",             // referrer
  "p": "/pricing",                       // path + query
  "t": "Pricing — Example",              // page title
  "w": 1440,                             // viewport width
  "props": { "utm_source": "newsletter" },
  "env": "production",
  "ts": 1739755562410
}

Everything else is automatic

Init once, get pageviews, SPA navigation, and outbound clicks for free.