[data-reveal]{opacity:1!important;transform:none!important}
Extending
Marketing

Extending

Read heatmaps and ship websites, funnels and history from another module, report events and variations from page code, and call the marketing HTTP API.

Three seams connect other code to the Marketing module: the @antelopejs/interface-dms-marketing interface for backend modules, the tracker's JavaScript API for visitor-site code, and the HTTP API.

Backend modules: the marketing interface

Every /modules/ page of the DMS is reserved to the platform owner. To show marketing data to tenant users, build your own page and read it through the interface. Declare it in optionalDependencies: calls reject when no module implements it, so catch and degrade.

Terminal
pnpm add --save-optional @antelopejs/interface-dms-marketing
src/heatmap.ts
import { GetClickHeatmap } from "@antelopejs/interface-dms-marketing";

export async function pricingHeatmap(tenantId: string) {
  try {
    return await GetClickHeatmap({
      tenantId, // an authenticated and authorized tenant id only
      path: "/pricing",
      from: "2026-09-01T00:00:00.000Z",
      to: "2026-09-30T23:59:59.999Z",
    });
  } catch {
    return null; // The page can work without analytics.
  }
}

The consumer reference defines the query, result types, date limits, element anchors and tenant boundaries. Resolve an element anchor against the rendered page when possible, and show an unavailable state separately from a successful heatmap with no clicks.

Keep the page itself backend-declared: compose it from DMS blocks, and make only the heatmap overlay a custom block with .meta({ name, description, icon }).

Shipping content from code

A project ships its websites, funnels and page snapshots as code, and a demo or a migration its history, through the same interface — never by writing the module's tables:

This global demo explicitly selects "default". All four content calls require a nonempty tenantId without surrounding whitespace, in TypeScript and at runtime; there is no fallback. start() runs per process, not per tenant creation. For new SaaS tenants, the application should register a durable created consumer before startup reconciliation (normally during construct(), without awaiting marketing calls there). See the lifecycle integration contract for operation deduplication, durable resource bindings and receipts. For existing tenants, use an inventory and the committed-provisioning gate instead of treating created as a backfill API. Marketing does not depend on SaaS or register consumers itself; callers still own authorization and readiness.

src/index.ts
import {
  EnsureFunnel,
  EnsureWebsite,
  ImportAnalytics,
} from "@antelopejs/interface-dms-marketing";

export async function start() {
  const tenantId = "default"; // Explicit global demo only.
  await EnsureWebsite({ tenantId, id: "shop-site", name: "Shop", domain: "shop.example.com" });
  await EnsureFunnel({
    tenantId,
    id: "shop-quote-request",
    websiteId: "shop-site",
    name: "Quote request",
    steps: [
      { kind: "url", value: "/pricing" },
      { kind: "custom", value: "quote_submitted" },
    ],
  });
  await ImportAnalytics({
    tenantId,
    importId: "shop-history",
    websiteId: "shop-site",
    sessions: [
      {
        visitor: "visitor-1",
        events: [
          { kind: "pageview", at: "2026-10-01T09:00:00Z", url: "https://shop.example.com/pricing" },
          { kind: "custom", at: "2026-10-01T09:02:00Z", url: "https://shop.example.com/pricing", name: "quote_submitted" },
        ],
      },
    ],
  });
}
  • EnsureWebsite and EnsureFunnel are idempotent and keyed by the id you choose: created, then unchanged on every later boot; updated when your declaration changed and the stored content still matches its hash; kept on owner divergence or concurrent change. Both refresh and hash reset compare the observed state at write time on MongoDB. A successful divergence reset clears the hash permanently; contention alone leaves it intact. See the reference for adapter requirements and state-comparison limits.
  • EnsurePageSnapshot({ tenantId, websiteId, url, html, viewport, document }) declares the backdrop of a page's heatmap when the tracker cannot capture it (a page not online, imported history). It is keyed by the page path and the layout of viewport, sanitized like a capture, and needs a website with snapshotsEnabled. A capture of the live page replaces it and is never overwritten (kept).
  • ImportAnalytics replays history through the live collection pipeline with its own timestamps. It runs once per importId and website; later calls with that id write nothing and answer in_progress while the first is replaying, then skipped (or failed if it failed). Split larger histories into chronological windows for the entire website. Concurrent imports with different ids for the same website reject, and an interrupted replay blocks further imports pending reconciliation. See the reference for the failure and rollout contract.
  • Await them from start() or later; a call made earlier waits for the module and its database.

The consumer reference has the contracts, the exact declaration rule and the import's limits.

Visitor sites: the tracker API

The tracker installs window.dmsMarketing on the tracked site. Guard every call with ?.: the object is absent when the script is blocked or fails to load.

app.js
window.dmsMarketing?.track("signup_submitted", { plan: "pro" });

window.dmsMarketing?.variation("hero-cta").then((arm) => {
  if (arm === "bold-cta") showAlternateHero();
});
  • track(name, data?) reports a custom event: a name up to 50 characters, optional data up to 4 KiB serialized. Custom events feed the overview and can be funnel steps.
  • variation(key) resolves to the arm the backend assigned this visitor, and reports the exposure. null means "render the control".
  • exposure(experiment, variation) reports an exposure for integrations that assign arms on their own.

Mark elements that page snapshots must never capture with data-dms-marketing-mask (text masked) or data-dms-marketing-block (replaced by an empty box). See Tracker & custom events.

HTTP API

All routes live under /api/marketing. The public ones serve visitor sites; the others need a DMS JWT.

RouteAuthPurpose
POST /collectpublicEvent ingestion
GET /tracker.jspublicThe tracker script
GET /experiments.js?website=publicPer-visitor A/B assignments, as a script
GET/POST/PUT/DELETE /websitestenant member / ownerWebsite CRUD
GET /stats/overview, /stats/pages, /stats/heatmaptenant memberDashboard, page inventory and heatmap reads
GET /funnels/:id/resultstenant memberFunnel and A/B computation
GET/PUT /settingsplatform ownerRuntime settings

The complete route table, with the event, snapshot and assignment contracts, is in Architecture.