This guide covers advanced integration scenarios for @rakuten-rewards/standdown-sdk. For the quick-start and API overview, see the README.
Before reading this guide, look at sample-extensions/session-manager/service-worker.js in this repository. It is a complete, production-oriented service worker that demonstrates:
- SDK initialization with policies and
ownAffiliatePatterns webNavigation.onCompleted,onErrorOccurred, andonCommittedlisteners (including multi-hop redirect handling)- A custom
SessionManagerfor tracking affiliate sessions - Cross-tab coordination via session state
- A
shouldStanddowndecision with areasonfield ('no_affiliate_detected','own_link','competitor_detected','session_active')
All patterns described in this guide are drawn from that implementation. Treat it as the canonical reference.
- Reference Implementation
- Detection Modes: webRequest vs Navigation-Only
- Supplying Policies
- Cross-tab session management
- Audit Log
- Handling Your Own Affiliate Links
The SDK operates in one of two modes depending on the browser it runs in. Mode selection is automatic; no configuration is required.
On Chrome, Firefox, and Edge, the SDK registers a webRequest.onBeforeRequest listener. This listener fires for every individual HTTP request in the main frame, including server-side 3xx redirect hops. As a result, redirectChain in the detection result contains every URL in the chain from the initial affiliate click-tracking hop to the final merchant URL:
redirectChain: [
'https://dpbolvw.net/click?...', // affiliate network hop (caught by onBeforeRequest)
'https://www.merchant.com/', // final destination (caught by onCommitted)
]
The SDK matches your policies against all URLs in the chain. This is the most complete detection mode.
Safari's webRequest stubs are callable but silently drop all listeners. The SDK detects this via navigator.vendor and falls back to webNavigation.onBeforeNavigate as the buffer source instead.
In this mode, onBeforeNavigate fires for the initiating URL (the affiliate link the user clicked), but server-side redirect hops resolve invisibly inside the browser. onCommitted fires for the final committed URL. As a result, redirectChain contains only two entries:
redirectChain: [
'https://dpbolvw.net/click?...', // entry URL (caught by onBeforeNavigate)
'https://www.merchant.com/', // committed URL (caught by onCommitted)
]
The intermediate hops are not visible, but affiliate tracking parameters embedded in the entry URL or surviving to the committed URL are still matched normally. Most affiliate networks include enough identifiers in these two URLs for reliable detection.
No code changes are required. The same checkForAffiliatePatterns(tabId) call works identically in both modes. The only observable difference is the length and content of result.redirectChain:
- On Chrome/Firefox, intermediate redirect hops are present.
- On Safari, only the entry URL and the committed URL are present.
If your integration inspects redirectChain directly (rather than relying on hasAffiliatePattern and matchedPatterns), be aware that chains will be shorter on Safari and intermediate hops will be absent.
The SDK does not bundle any default affiliate network policies. You must supply the NetworkPolicy objects for every affiliate network you want to detect, passing them via config.policies at construction time.
import { StanddownSDK } from '@rakuten-rewards/standdown-sdk';
import type { NetworkPolicy } from '@rakuten-rewards/standdown-sdk';
const CJ_POLICY: NetworkPolicy = {
id: 'cj',
schemaVersion: 2,
policyVersion: 1,
network: {
id: 'cj',
name: 'Commission Junction (CJ)',
sessionDuration: 1_800_000, // 30 minutes in ms
},
rules: [
{ domain: 'dpbolvw.net', reason: 'CJ primary click-tracking domain' },
{ domain: 'anrdoezrs.net', reason: 'CJ click-tracking domain variant' },
{ domain: 'jdoqocy.com', reason: 'CJ click-tracking domain variant' },
{ domain: 'tkqlhce.com', reason: 'CJ click-tracking domain variant' },
{ params: 'cjevent', reason: 'CJ event tracking parameter on destination URL' },
],
};
const AFSRC_POLICY: NetworkPolicy = {
id: 'generic-afsrc',
schemaVersion: 2,
policyVersion: 1,
network: {
id: 'afsrc',
name: 'Generic Affiliate Source',
sessionDuration: 1_800_000,
},
rules: [
{ params: 'afsrc', reason: 'Cross-network affiliate source parameter' },
],
};
const shield = new StanddownSDK({ policies: [CJ_POLICY, AFSRC_POLICY] });Each policy is validated at initialization time. Invalid policies (missing required fields, unsupported schemaVersion, invalid rules) are skipped with a console.warn. If no valid policies are loaded, the SDK emits a console.warn and checkForAffiliatePatterns will always return no-match.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Unique identifier for this policy (e.g. "cj"). |
schemaVersion |
number |
Yes | Must be 2. Policies with an unrecognised version are rejected at load time. |
policyVersion |
number |
Yes | Positive integer content version. Increment when detection behaviour changes substantially. |
network.id |
string |
Yes | Short machine-readable network identifier. Appears as match.network in DetectionResult. |
network.name |
string |
Yes | Human-readable network name. |
network.sessionDuration |
number |
No | How long an affiliate session should be honoured, in milliseconds. Used to compute result.expiresAt. Defaults to 30 minutes if omitted. |
rules |
PolicyRule[] |
Yes | Detection rules for this network. |
metadata |
object |
No | Freeform metadata (e.g. lastUpdated, documentation). Not used by the SDK. |
Within a single rule, all present fields must match (AND semantics, evaluated in this order):
| Field | Matches when |
|---|---|
domain |
Hostname equals the value exactly, or is a subdomain of it (case-insensitive) |
paths |
URL pathname starts with any entry in the array (OR semantics) |
params |
URL has any of the listed query parameter names (OR semantics) |
pattern |
Full URL matches the regex string (compiled case-insensitively at load time) |
Across the full redirect chain, all per-URL matches are collected. Per URL, the first matching rule wins (early exit for that URL only).
ReDoS warning: The
patternfield must not use constructs that cause catastrophic backtracking (e.g.(a+)+,(x+x+)+y). The SDK runs inside a single-threaded service worker; a hung regex will block the entire event loop. Always use bounded quantifiers and avoid nested repetition.
The classical affiliate activation model works like this: a user clicks an affiliate link, their browser follows a redirect chain through the network's click-tracking hop, and they arrive at the merchant page with the affiliate session attributed in the URL (e.g. via query parameters or cookies). checkForAffiliatePatterns(tabId) on the user's current tab captures this chain directly.
Some extensions use a different model: they open a background tab, navigate it through the affiliate network's redirect chain to set the affiliate cookies on the merchant domain, and then close it. The user's visible tab never participates in the redirect; it simply loads the merchant page directly and inherits the cookie-based session that was established in the background. Because the visible tab saw no affiliate redirect, calling checkForAffiliatePatterns on it returns no match.
This creates a gap: the tab that carries evidence of the affiliate activation (the background tab) is closed before the user ever arrives at the merchant. The only way to bridge this gap is to monitor all navigations on all tabs, detect the activation on the background tab while it is still open, record that activation, and then check the recorded state when the user's visible tab subsequently loads the merchant page.
The SDK observes every tab automatically; you do not need to filter to visible tabs. Call checkForAffiliatePatterns in a webNavigation.onCompleted listener that runs for every tab, capture any match, and store it against the merchant domain:
import { StanddownSDK } from '@rakuten-rewards/standdown-sdk';
const shield = new StanddownSDK({ policies: MY_POLICIES });
// Monitor ALL tabs; the background tab that carries the affiliate redirect
// will be caught here before it closes.
chrome.webNavigation.onCompleted.addListener(({ tabId, frameId }) => {
if (frameId !== 0) return; // top-level frame only
const result = shield.checkForAffiliatePatterns(tabId);
if (!result.hasAffiliatePattern) return;
recordActivation(result); // store against merchant domain for later lookup
});Detecting the background activation is only half the work. When the user's visible tab later loads the same merchant, you need to look up whether a prior activation was recorded for that domain and act on it. This lookup-and-decide flow is what constitutes a session in this model.
The SDK does not manage this session state; it only answers "did this tab see an affiliate redirect?" Your extension is responsible for building the bridge: record activations keyed by merchant domain, and query that record when a new tab reaches the same domain. A few design decisions to make:
- Scope: Should the suppression apply per-domain, or globally across all tabs?
- Duration: How long should a recorded activation remain active? Until the first matching tab loads? For a fixed TTL?
- Domain keying: How do you normalize merchant domains? (
shop.example.comandwww.example.comshould typically resolve to the same key.)
The pattern below is a starting point. It records activations by merchant hostname and checks them when any tab finishes loading:
import { StanddownSDK } from '@rakuten-rewards/standdown-sdk';
const shield = new StanddownSDK({ policies: MY_POLICIES });
// Simple in-memory record: merchant hostname → detected networks
const pendingActivations = new Map<string, string[]>();
// Monitor ALL tabs in a single listener.
// On every completed navigation:
// 1. Check if this tab (possibly background) saw an affiliate redirect → record it.
// 2. Check if a prior activation was recorded for the merchant this tab just loaded → stand down.
chrome.webNavigation.onCompleted.addListener(({ tabId, frameId, url }) => {
if (frameId !== 0) return; // top-level frame only
const result = shield.checkForAffiliatePatterns(tabId);
if (result.hasAffiliatePattern) {
// Record the activation keyed by the merchant page at the end of the redirect chain.
for (const match of result.matchedPatterns) {
try {
const merchant = new URL(result.redirectChain[result.redirectChain.length - 1]).hostname;
const existing = pendingActivations.get(merchant) ?? [];
if (!existing.includes(match.network)) {
pendingActivations.set(merchant, [...existing, match.network]);
}
} catch { /* malformed URL */ }
}
}
// Check whether a background activation was recorded for the merchant this tab just loaded.
try {
const hostname = new URL(url).hostname;
const networks = pendingActivations.get(hostname);
if (networks) {
// A competing network already owns this merchant session; stand down.
pendingActivations.delete(hostname); // consume: one stand-down per activation
markTabAsStanddown(tabId, networks);
}
} catch { /* malformed URL */ }
});Session management is your responsibility. The example above uses a simple consume-once model: the first tab to load the merchant after a background activation is suppressed, and the record is cleared. A production implementation will need more: TTL-based expiry so stale records don't accumulate, root-domain normalization so
shop.example.comandwww.example.comresolve to the same key, and per-domain LRU eviction to bound memory. Seesample-extensions/session-manager/session-manager.jsfor a reference implementation of aSessionManagerclass with all of these properties, andservice-worker.jsin the same directory for how it is wired into the navigation listeners.
The SDK includes an optional audit log that records affiliate detections to chrome.storage.local. When enabled, every call to checkForAffiliatePatterns that returns a match is automatically recorded with no extra code needed. Entries are keyed by root domain, survive service worker restarts, and expire after each network's sessionDuration elapses.
Use the async factory StanddownSDK.create() to instantiate the SDK with the audit log enabled. The factory hydrates in-memory state from chrome.storage.local before returning, so getEventLog() and getEventsByDomain() return accurate results immediately, even after a service worker restart.
import { StanddownSDK } from '@rakuten-rewards/standdown-sdk';
// Instantiate once at service worker startup.
const shield = await StanddownSDK.create({ policies: MY_POLICIES, enableAuditLog: true });Manifest permission: Add
"storage"to yourmanifest.jsonpermissionsarray alongside"webNavigation"and"tabs".
Top-level await caveat: Chrome MV3 service workers can idle-terminate between events. If your extension must handle its very first navigation event after a cold start, consider wiring listeners before awaiting
create(), or initialising synchronously withnew StanddownSDK({ policies: MY_POLICIES, enableAuditLog: true })and accepting that the in-memory log will not contain entries from before the last restart until storage hydration completes asynchronously.
import type { AffiliateEvent } from '@rakuten-rewards/standdown-sdk';
// All active (non-expired) entries across every domain
const log: AffiliateEvent[] = shield.getEventLog();
// Entries for a specific domain; accepts a full URL or a bare hostname
const events: AffiliateEvent[] = shield.getEventsByDomain('https://www.example.com/');
// or:
const events2: AffiliateEvent[] = shield.getEventsByDomain('example.com');
for (const event of events) {
console.log(event.url); // landing URL that triggered detection
console.log(event.timestamp); // Unix ms when detection occurred
console.log(event.sessionDuration); // ms until the session expires
console.log(event.matchedPatterns); // MatchedPattern[]: same shape as DetectionResult
console.log(event.redirectChain); // string[]: full URL chain observed
console.log(event.isOwnAffiliateLink); // boolean: mirrors DetectionResult at detection time
}getEventsByDomain() normalises the input to a root domain (e.g. shop.example.com → example.com) and returns [] when no active entry exists for that domain. It throws if the audit log was not enabled at construction time.
Entries expire after the matched network's sessionDuration (defined on NetworkPolicy.network.sessionDuration, in milliseconds). Expiry is checked lazily; an entry is only pruned when it is read. To force a full sweep, call getEventLog().
The audit log is most useful when your extension needs to know whether a prior affiliate session exists for a domain after a service worker restart, where an in-memory session store would have been wiped. If the user visits a merchant, closes their browser, reopens it, and navigates back to the same merchant, an in-memory SessionManager has no record of the prior visit; the audit log does.
It can also simplify integrations that only need to answer "has this domain been activated recently?" without building a full session management layer. Rather than maintaining a Map of activations yourself, you delegate recording and expiry to the SDK and query on demand.
If your stand-down logic requires more than a simple domain lookup (for example, pairing background-tab activations to visible tabs, combining live detection with prior session state, or applying custom expiry rules), a custom SessionManager (as shown in Cross-tab session management) gives you more control. The two approaches are not mutually exclusive: you can enable the audit log for persistence while also maintaining in-memory state for real-time coordination.
sample-extensions/audit-log/service-worker.js demonstrates the audit log pattern in a complete extension.
If your extension operates on one of the networks you have configured, checkForAffiliatePatterns will naturally detect your own affiliate links. The correct approach is to keep all policies active and use result.isOwnAffiliateLink to distinguish your links from competitors'.
Pass your publisher-specific identifiers as RegExp patterns when creating the SDK. The SDK tests every URL in the redirect chain against your patterns and sets result.isOwnAffiliateLink automatically.
import { StanddownSDK } from '@rakuten-rewards/standdown-sdk';
// Replace these with the publisher-specific parameters assigned to YOUR extension
// by each affiliate network. The values below are fictional examples.
//
// How to find your identifiers:
// CJ: Account Settings → Publisher IDs → m_si (site ID), m_pl (publisher label)
// eBay: Partner Network dashboard → Custom ID / tracking ID
// Other networks: inspect landing-page URLs after a test click for unique params
const shield = new StanddownSDK({
policies: MY_POLICIES,
ownAffiliatePatterns: [
/m_pl=YourExtension/, // CJ: publisher label
/[?&]m_si=12345/i, // CJ: publisher site ID
/customid=yourextid/, // eBay Partner Network: custom tracking ID
// For networks with publisher ID in an intermediate hop's path:
// /goto\.target\.com\/c\/12345\//,
],
});
// In your message handler: stand down only for competitors, not own links.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type !== 'CHECK_STANDDOWN') return false;
const tabId = sender.tab?.id;
if (tabId == null) return false;
const result = shield.checkForAffiliatePatterns(tabId);
sendResponse({
shouldStanddown: result.hasAffiliatePattern && !result.isOwnAffiliateLink,
});
return true;
});All URLs in the redirect chain are tested, not just the final URL, so publisher IDs embedded in intermediate hops are handled correctly. The SDK applies OR semantics across all patterns and all URLs: any single match sets result.isOwnAffiliateLink: true.
Manual approach (fallback): If you need to determine own-link status outside of the SDK (e.g. based on context not available at construction time), inspect
result.redirectChainandresult.matchedPatternsdirectly and apply your own logic.
eBay injects a customid query parameter on the destination URL. Use a regex that matches the parameter and your specific value:
/customid=yourextid/ // matches ?customid=yourextid anywhere in the URLFor exact parameter matching (avoids partial matches like customid=yourextid2):
/[?&]customid=yourextid(?:&|$)/Target affiliate links route through goto.target.com, where the publisher ID appears as a path segment (e.g. /c/12345/). Because this ID is on an intermediate hop (not the final destination URL), a path regex is required. The SDK tests all URLs in the chain, so this is handled correctly:
/goto\.target\.com\/c\/12345\// // matches the intermediate hop with your publisher IDConfigure all your per-network patterns in a single ownAffiliatePatterns array:
const shield = new StanddownSDK({
policies: MY_POLICIES,
ownAffiliatePatterns: [
/m_pl=YourExtension/, // CJ: publisher label
/[?&]m_si=12345/i, // CJ: publisher site ID
/customid=yourextid/, // eBay Partner Network
/goto\.target\.com\/c\/12345\//, // Target: path-based publisher ID (intermediate hop)
],
});