Sending Engagements on Web
Overview
- Promoted uses the Snowplow web client for sending records from web browsers to your web proxy.
- Your web proxy forwards the Snowplow message to our Snowplow-compatible Metric API endpoint
1. Add Snowplow tracker
Promoted uses the Snowplow client because it has built-in support for batching, retries and privacy settings.
npm install @snowplow/browser-tracker
npm install promoted-snowplow-logger
Add this to a utility file like promoted.ts
.
import { newTracker, trackPageView, trackSelfDescribingEvent } from '@snowplow/browser-tracker';
import { createEventLogger } from 'promoted-snowplow-logger';
export const promotedLoggingEnabled = false;
const isProduction = process.env.NODE_ENV === 'production';
const appId = isProduction ? 'yourmarket-prod' : 'yourmarket-dev';
const cookieSecure = isProduction;
const cookieSameSite = isProduction ? 'None' : 'Lax';
// Can be undefined on the server.
let hostname: string | undefined;
let snowplowLoggerUrlPrefix: string | undefined;
if (typeof window !== 'undefined' && typeof window?.location !== 'undefined') {
const { location } = window;
hostname = location.hostname;
snowplowLoggerUrlPrefix = `${location.origin}/logger`;
}
// Should not be used on the server-side.
const tracker = newTracker('cf', snowplowLoggerUrlPrefix, {
appId,
cookieSecure,
cookieSameSite,
cookieDomain: hostname,
contexts: {
webPage: true,
session: false,
},
maxLocalStorageQueueSize: 100,
// Has other interesting options like postPath, stateStorageStrategy.
})
// Throw when developing.
const throwError =
(typeof window?.location !== "undefined" && window?.location?.hostname === "localhost");
export const handleError: (err: Error) => void = throwError ? (err) => { throw err; } : console.error;
export const eventLogger = createEventLogger({
enabled: promotedLoggingEnabled,
snowplow: {
trackSelfDescribingEvent,
trackPageView,
},
handleError,
// Optional - if you have custom anonymous userId. This overrides the anonUserId on calls through eventLogger.
// This does not work with auto tracking.
getUserInfo: () => ({
anonUserId: getAnonymousUserId()
}),
});
2. Create a web proxy endpoint that forwards the Snowplow HTTP requests to Promoted’s Metrics Snowplow API
We recommend platforms host a web endpoint that forwards the Snowplow requests to Promoted’s servers. We recommend the marketplace do this forwarding so (1) you can authenticate/validate events and (2) reduces the chance of the endpoint getting blocked by future browser extensions.
Here is an example using NextJS. You want to serve this depending on snowplowLoggerUrlPrefix
and postPath
. E.g. if the logger url is /logger
, then you'll want to serve this from
import axios from 'axios';
const snowplowEndpoint = 'https://prod.given.by.promoted.ai/sp'; // From env or config.
const apiKey = 'PROMOTED_PROD_METRICS_API_KEY'; // From secret store.
// TODO - review optimizations in this link. Also contains examples using `node-fetch` and `got` examples.
// https://github.com/promotedai/promoted-ts-client#using-node-fetch
export default async (req, res) => {
// TODO - authenticate the events are from this user.
// TODO - prevent Requests and Insertions from being logged to this endpoint.
const logResponse = await axios.post(
snowplowEndpoint,
{
// If the Snowplow payload is a JSON object, use `sp`.
// If the Snowplow payload is a JSON string, use `sps`.
sp: req.body,
ua: req.headers['user-agent'],
},
{
headers: {
"x-api-key": apiKey,
},
timeout: 5000,
});
res.statusCode = logResponse.status
res.json({ status: logResponse.statusText })
}
If creating a proxy is a challenge, please sync with the Promoted team. This is easy to do with Nodejs and NextJS servers. If required, it's also possible to log to Promoted by having registering a first party DNS record that sends events to Promoted or use Promoted's URLs directly.
3. Log Impression and Actions
- Impressions = When an item is in the viewport long enough to be considered viewed by a user.
- Actions = When a user acts on something (e.g. clicks on item, links, like, purchase, email sign up, expand into details, etc.).
We provide impression trackers that leverage the HTML5 IntersectionObserver to notify us when an item is viewed for an adequate duration.
For web, we have a small React hook / HOC to help log impressions. It uses an Intersection Observer to get notifications about when an item is viewed long enough.
import { useImpressionTracker } from "impression-tracker-react-hook";
import { eventLogger, handleError } from "../../utils/promoted";
// React Component.
function Product(props) {
const contentId = "<productid>";
const [impressionRef, impressionId, logImpressionFunctor] = useImpressionTracker({
insertionId: props.insertionId,
contentId,
logImpression: eventLogger.logImpression,
handleError,
});
return (
<div
ref={impressionRef}
onClick={() => {
// In case the item was clicked too quickly, log an impression.
logImpressionFunctor();
eventLogger.logClick({
impressionId,
contentId,
targetUrl: "...",
// Not required. HTML element ID
elementId: "...",
});
}}
>
...
</div>
);
}
promoted-snowplow-logger
also supports logView
and logAction
.
Some actions, like purchases, will be more secured if logged from server-side. Here's a guide for Sending Engagements from your server.
The contentId is required, but the elementId is not.
The elementId is the ID of the HTML element. It is supported, but not required or useful for Promoted.
Optional - Set up and log (Page) Views
Logging (page, screen or activity) Views are not required initially. Promoted does not currently use Views when ranking content.
Promoted might ask you to log Views to:
- Help with debugging.
- Help us improve the quality of joins if you issue multiple Requests on the same page.
Example for search page
eventLogger.logView({
name: "Product search",
useCase: "SEARCH",
searchQuery: "shoes",
});
Example for product page
eventLogger.logView({
name: "Product page",
useCase: "CLOSE_UP", // or leave it blank.
contentId: "abc",
});
The default Snowplow tracker parameters causes the tracker to send Promoted browser fields including user_agent, url, referrer. If you do not want these fields sent to Promoted, please let Promoted know.
If you have a Single Page Application, you can also move the trackPageView call to a route that indicates a different page
Optional - Automatic Link Tracking
The Snowplow tracker has a way of automatically tracking all links. This does not yet work with custom anonUserIds. Here is a code snippet to add to the script tag to enable this.
<script type="text/javascript">
// Optional - enables tracking of links. Also looks impressionId on <a> tags.
// This allows us to attribute clicks to impressions.
window.snowplow("enableLinkClickTracking", null, null, true, [
(element) => {
// Construct the context.
const impressionId = element?.dataset?.impressionId?.value;
if (impressionId) {
return {
schema: "iglu:ai.promoted/impression_cx/jsonschema/1-0-0",
data: {
impressionId,
},
};
} else {
return undefined;
}
},
]);
// Optional - automatic update tracking links when the dom changes.
// Delay attach a MutationObserver to update automatic link tracker.
const setupRefresh = () => {
if (MutationObserver) {
var observer = new MutationObserver(() => {
window.snowplow("refreshLinkClickTracking");
});
var target = document.querySelector("body");
if (!target) {
// Delay if body is not ready.
window.setTimeout(setupRefresh, 250);
return;
}
observer.observe(target, {
subtree: true,
childList: true,
});
}
window.snowplow("refreshLinkClickTracking");
};
// Delay setup until after page load.
window.setTimeout(setupRefresh, 250);
</script>
Anchor tags that are automatically tracked won’t have impressionIds on them automatically. You will need to add an attribute that enableLinkClickTracking context function resolves.
return (
<div ref={impressionRef}>
<a href="blah" data-impression-id={impressionId}>
...
</a>
</div>
);
Updated 2 months ago