Shop It Docs
Developer Resourcesbanners

Banners Targeting Engine

Banners Targeting Engine

The Banners module includes a server-side contextual targeting engine. When a public serving request arrives at GET /api/banners/serve/:slug, the engine extracts a targeting context from the HTTP request headers and evaluates all active targeting rules for each candidate campaign. A campaign passes only if ALL of its rules evaluate to true for the given context.


1. Targeting Context Extraction

Context is extracted entirely from HTTP request headers — no client-sent body data. The implementation is in BannerServingService.buildTargetingContext(req).

DimensionSource HeaderExtraction Logic
deviceTypeUser-AgentRegex: /iPad|Android(?!.*Mobile)/i"tablet"; /Mobile|Android|iPhone|iPod/i"mobile"; otherwise → "desktop". Empty UA → "desktop".
countryCodex-country-code (fallback: cf-ipcountry)First non-null value. null if both absent.
isLoggedInAuthorizationPresence of Authorization: Bearer ... header → true. Token is NOT verified.
languageAccept-LanguagePrimary tag: split on ,, take first, split on [-;], lowercase. null if absent.
isNewVisitorx-device-id (or x-session-id)Absence of both headers → true. Presence of either → false.
referrerDomainReferernew URL(referer).hostname. null if absent or unparseable.
hourOfDayserver clocknew Date().getHours() (local time)
dayOfWeekserver clocknew Date().getDay() (0=Sunday, 6=Saturday)

userSegment is defined in the type but always null — the user_segment rule type evaluation is deferred to SP5.


2. Targeting Context Type

interface TargetingContext {
  deviceType: "mobile" | "tablet" | "desktop";
  countryCode: string | null;
  isLoggedIn: boolean;
  language: string | null;
  hourOfDay: number;          // 0–23, local
  dayOfWeek: number;          // 0=Sunday, 6=Saturday, local
  isNewVisitor: boolean;
  referrerDomain: string | null;
}

3. Rule Types

The implementation lives in BannerServingService.evaluateSingleRule(rule, ctx). Each rule has a ruleType, operator, and value (jsonb). 9 rule types are defined in the targeting_rule_type enum:

ruleTypeValue typeValid operators
devicestring[] (e.g., ["mobile", "tablet"])in, not_in
countrystring[] (ISO 3166-1 alpha-2, uppercase)in, not_in
user_segmentany(any operator — always returns true)
login_statebooleaneq, not_in
languagestring[] (lowercased)in, not_in
hour_of_daynumber or [number, number]between, eq, lt, gt
day_of_weeknumber[] (0=Sunday, 6=Saturday)in, not_in
new_visitorbooleaneq
referrer_domainstring[] (substring match)in, not_in, contains

3.1 device

Example RulePasses When
operator: "in", value: ["mobile", "tablet"]Device is mobile or tablet
operator: "not_in", value: ["desktop"]Device is not desktop

3.2 country

Example RulePasses When
operator: "in", value: ["NP", "IN"]Country is Nepal or India (uppercase compare)
operator: "not_in", value: ["US"]Country is not USA

Null behavior: if countryCode is null, the rule passes (returns true). This is a fail-open behavior — campaigns with country: in [...] rules will NOT be filtered out for requests without a country header.

3.3 login_state

Example RulePasses When
operator: "eq", value: trueClient sent a Bearer Authorization header
operator: "not_in", value: trueClient did NOT send a Bearer token

3.4 language

Example RulePasses When
operator: "in", value: ["ne", "en"]Primary Accept-Language is Nepali or English
operator: "not_in", value: ["zh"]Language is not Chinese

Null behavior: if language is null, the rule passes (fail-open).

3.5 hour_of_day

Example RulePasses When
operator: "between", value: [9, 17]Current hour is 9 through 17 inclusive
operator: "lt", value: 6Current hour < 6 (night)
operator: "gt", value: 20Current hour > 20 (late evening)
operator: "eq", value: 12Current hour is exactly 12

3.6 day_of_week

Example RulePasses When
operator: "in", value: [1, 2, 3, 4, 5]Weekday (Mon–Fri)
operator: "in", value: [0, 6]Weekend
operator: "not_in", value: [0]Not Sunday

3.7 new_visitor

Example RulePasses When
operator: "eq", value: trueNo x-device-id AND no x-session-id header present
operator: "eq", value: falseEither header present

3.8 referrer_domain

Example RulePasses When
operator: "in", value: ["google.com"]referer hostname contains "google.com" (substring)
operator: "not_in", value: ["nomor.tech"]referer does NOT contain "nomor.tech"
operator: "contains", value: ["facebook"]Same as in — substring match

Null behavior: if referrerDomain is null, returns true if operator is not_in, otherwise returns false (i.e., in and contains return false for absent referrer).

3.9 user_segment

Always returns true. Defined in schema and accepted via API. No filtering at this stage. Future implementation will resolve user segment from session/customer service and evaluate against a stored segment list.


4. Evaluation Algorithm

For each active campaign C:
  rules = campaign_targeting_rules WHERE campaign_id = C.id AND is_active = true

  If rules is empty:
    → campaign PASSES (serve to everyone)

  For each rule R in rules:
    result = evaluateSingleRule(R, context)
    If result is FALSE:
      → campaign FAILS (skip; not included in banners)

  If all rules passed:
    → campaign PASSES

AND logic: a campaign with 3 rules passes only if all 3 pass. There is no OR grouping at the rule level. For OR-style targeting, create two separate campaigns.

Fail-open on exceptions: if evaluateSingleRule throws (e.g., malformed value jsonb), the rule is treated as passing and a warn is logged:

try { /* evaluation */ } catch {
  this.logger.warn(`Targeting rule evaluation failed ruleType=${ruleType} operator=${operator}`);
  return true;  // fail-open
}

5. Operator–RuleType Compatibility Matrix

Validated by TargetingAdminService.VALID_OPERATORS (module-level constant) when creating or updating a rule. Throws TARGETING_RULE_INVALID_OPERATOR on mismatch.

Rule Typeeqnot_ininbetweenltgtcontains
device
country
login_state
language
hour_of_day
day_of_week
new_visitor
referrer_domain
user_segment(any)(any)(any)(any)(any)(any)(any)

6. Targeting Hash and Cache Key Segmentation

Different clients receive different banners depending on their targeting context. Caching a single response per placement slug would serve the wrong banners to users with different contexts.

The serving endpoint uses a targeting hash to create per-context cache segments:

const str = [
  ctx.deviceType,
  ctx.countryCode ?? "",
  ctx.isLoggedIn ? "1" : "0",
  ctx.language ?? "",
  ctx.hourOfDay,
  ctx.dayOfWeek,
  ctx.isNewVisitor ? "1" : "0",
  ctx.referrerDomain ?? "",
].join("|");
const hash = createHash("sha256").update(str).digest("hex").slice(0, 16);
const cacheKey = CacheKeyUtil.build("banners:serve", [
  ["slug", slug],
  ["ctx", hash],
]);

Examples:

  • Mobile user in Nepal: banners:serve:homepage-hero:a3f9c1d4...
  • Desktop user in India: banners:serve:homepage-hero:7b2e8a91...

Each is independently cached and invalidated.

16-character hex prefix provides 2^64 possible hash values — negligible collision probability.


7. Cache Invalidation After Schedule Sync

When the SYNC_SCHEDULE_STATUS BullMQ job runs and changes any banner or campaign status, the serving cache may contain stale data. The processor calls:

await redisCacheService.invalidatePattern("banners:serve:*");

This invalidates all serving cache entries for all placements and all contexts simultaneously. The next request to any placement re-queries the database.

Failure handling: invalidatePattern errors are caught and logged as warn — not rethrown. The 60-second TTL (BANNER_SERVE_CACHE_TTL_SECONDS) ensures eventual consistency.


8. Adding New Rule Types

To add a new targeting rule type:

  1. Add the new value to the targeting_rule_type enum in packages/db/src/schema/banners/targeting.ts
  2. Generate and run a Drizzle migration
  3. Add an entry to the VALID_OPERATORS map in TargetingAdminService (for validation)
  4. Add a case to evaluateSingleRule() in BannerServingService
  5. Update TargetingAdminService.validateOperators() if the rule has value-shape constraints
  6. Add a unit test in __tests__/unit/banner-serving.service.unit.spec.ts covering the new rule type
  7. Update this documentation with the new rule type spec

No changes are required to the campaign_targeting_rules schema itself — the value jsonb column is flexible.