<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>SFCC Field Notes - Oleksandr Katerynchuk</title>
  <subtitle>Notes on Salesforce Commerce Cloud (SFCC) architecture, development, and platform internals.</subtitle>
  <link href="https://okater.com/feed.xml" rel="self"/>
  <link href="https://okater.com/"/>
  <updated>2026-10-08T00:00:00Z</updated>
  <id>https://okater.com/</id>
  <author>
    <name>Oleksandr Katerynchuk</name>
  </author>
  <entry>
    <title>Bot Management Automation for SFCC eCDN with Logpush</title>
    <link href="https://okater.com/posts/sfcc-bot-management-automation/"/>
    <updated>2026-10-08T00:00:00Z</updated>
    <id>https://okater.com/posts/sfcc-bot-management-automation/</id>
    <content type="html">&lt;p&gt;In the &lt;a href=&quot;https://okater.com/posts/sfcc-logpush-jobs/&quot;&gt;Logpush article&lt;/a&gt; I said you can act on your eCDN logs, not just store them. Acting can mean a lot of things: a dashboard that shows who your traffic really is, a look back at what happened during last month&#39;s sale, or a firewall rule. This article is about that last one, and about automating it.&lt;/p&gt;
&lt;p&gt;The short version: Cloudflare already gives every request a bot score, and Logpush ships that score to your bucket. You just can&#39;t use it in a firewall rule. So I built a small proof of concept that reads the score from the logs, finds the IPs that keep scoring like bots, and writes eCDN firewall rules for them. A second loop removes those rules once they stop matching.&lt;/p&gt;
&lt;aside class=&quot;callout&quot; role=&quot;note&quot;&gt;
&lt;p&gt;&lt;strong&gt;Before we start.&lt;/strong&gt; This is my personal POC, not a Salesforce product or a supported feature. Automated firewall rules can lock real shoppers out of your store. Treat everything below as a pattern to discuss with your team, and try it on a sandbox, never on production.&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;Here is the demo:&lt;/p&gt;
&lt;div class=&quot;video-embed&quot;&gt;&lt;iframe src=&quot;https://customer-s2hy6afr8h81a1gk.cloudflarestream.com/5054df4cced8caf49e0a95cdbc9c3222/iframe?poster=https%3A%2F%2Fcustomer-s2hy6afr8h81a1gk.cloudflarestream.com%2F5054df4cced8caf49e0a95cdbc9c3222%2Fthumbnails%2Fthumbnail.jpg%3Ftime%3D%26height%3D600&quot; title=&quot;Bot Management Automation POC for SFCC eCDN&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;h2 id=&quot;the-score-is-in-the-logs%2C-not-in-the-rules&quot; tabindex=&quot;-1&quot;&gt;The score is in the logs, not in the rules &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#the-score-is-in-the-logs%2C-not-in-the-rules&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Every HTTP request record in Logpush carries a &lt;code&gt;BotScore&lt;/code&gt; from 1 to 99. Close to 1 is almost certainly a bot, close to 99 is almost certainly a human. In the demo there&#39;s a request for a pizza page on my sandbox (no idea why) that scored 2, with &lt;code&gt;BotScoreSrc&lt;/code&gt; set to Machine Learning.&lt;/p&gt;
&lt;p&gt;The obvious next step would be a rule like &amp;quot;challenge anything with a score below 30&amp;quot;. The Cloudflare field for that is &lt;code&gt;cf.bot_management.score&lt;/code&gt;, and if you type it into the Business Manager expression editor, you get an error. Enforcing on bot score is part of Cloudflare Bot Management, a paid product with its own entitlement. On the eCDN the score is informational: you can read it, but you can&#39;t act on it at the edge.&lt;/p&gt;
&lt;p&gt;What eCDN rules do understand is IPs, ASNs, countries, paths and user agents. So the workaround is to move the decision out of the edge. Read the score after the fact and translate it into something a rule can match, like an IP address.&lt;/p&gt;
&lt;h3 id=&quot;what-about-the-new-known-bot-field%3F&quot; tabindex=&quot;-1&quot;&gt;What about the new Known Bot field? &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#what-about-the-new-known-bot-field%3F&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://help.salesforce.com/s/articleView?id=release-notes.rn_b2c_ecdn_verified_bots.htm&amp;amp;release=262&amp;amp;type=5&quot;&gt;Release 26.9&lt;/a&gt; added two fields to eCDN custom firewall and rate limiting rules. &lt;code&gt;cf.client.bot&lt;/code&gt; is true for any verified bot, and &lt;code&gt;cf.verified_bot_category&lt;/code&gt; narrows it down to a category like Search Engine Crawler, AI Crawler, AI Search, Feed Fetcher or Aggregator. Business Manager calls it &lt;strong&gt;Known Bot&lt;/strong&gt;, and both fields work in the CDN Zones API too.&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://okater.com/images/posts/sfcc-bot-management-automation/known-bot-field.webp&quot; alt=&quot;Business Manager custom firewall rule editor with the Known Bot field selected and the expression preview showing cf.client.bot&quot; width=&quot;731&quot; height=&quot;241&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;Known Bot in the rule editor. One click, no ASN lists.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;These are useful. Until now, the usual trick to keep Googlebot and Bingbot out of a rate limit was to exclude Google&#39;s and Microsoft&#39;s ASNs by number. Now you can exempt verified bots directly, or treat AI crawlers differently from search engines if your business wants that.&lt;/p&gt;
&lt;p&gt;But verified bots are the polite ones. They announce themselves, and Cloudflare checks they are who they claim to be. The traffic that hurts (scrapers, credential stuffers, inventory hoarders) announces nothing. The release note says it plainly: other Bot Management fields, including bot score, still require the separate entitlement. Known Bot is how you let the good bots through. It doesn&#39;t catch the bad ones.&lt;/p&gt;
&lt;h2 id=&quot;the-idea-in-one-picture&quot; tabindex=&quot;-1&quot;&gt;The idea in one picture &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#the-idea-in-one-picture&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Strip away the SFCC details and the concept looks like this:&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
flowchart LR
    V[Visitors] --&gt; EDGE[CDN edge&lt;br /&gt;firewall rules]
    EDGE --&gt; O[Origin]
    EDGE -. request logs .-&gt; ST[(Log storage)]
    EDGE -. firewall event logs .-&gt; ST
    ST --&gt; D[Detect&lt;br /&gt;low score + high volume]
    ST --&gt; C[Clean up&lt;br /&gt;rules gone quiet]
    B[Business thresholds&lt;br /&gt;+ human review] -.-&gt; D
    D --&gt;|create or update rules| API[CDN API]
    C --&gt;|remove rules| API
    API --&gt; EDGE
&lt;/pre&gt;
&lt;p&gt;The edge logs every request. One function reads the request logs, finds IPs that are both busy and bot-like, and asks the CDN API to put them in a rule. Another function reads the firewall event logs and removes entries nobody has hit for a while. The business sets the thresholds, and a person decides when a rule goes from watching to blocking.&lt;/p&gt;
&lt;p&gt;Nothing here is specific to Commerce Cloud. Any CDN that gives you request logs with a bot score and an API for rules can work this way.&lt;/p&gt;
&lt;h2 id=&quot;what-the-poc-does&quot; tabindex=&quot;-1&quot;&gt;What the POC does &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#what-the-poc-does&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For the demo I built it as an SFCC cartridge with a custom job, because that was the fastest way to show the whole loop in one place. The job reads the files the HTTP requests Logpush job drops into S3, and every setting is a job parameter:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://okater.com/images/posts/sfcc-bot-management-automation/job-config.webp&quot; alt=&quot;Business Manager job step parameters: BotScoreThreshold 30, MinRequests 20, WindowMinutes 15, BlockAction log&quot; width=&quot;354&quot; height=&quot;260&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;The default detection settings. Every threshold is a job parameter, so tuning doesn&#39;t need a deploy.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bot score threshold.&lt;/strong&gt; Requests below this count as bot-like. The default is 30. In the video I raised it to 70 on purpose so that my small sandbox traffic would trigger something. That&#39;s far too aggressive for real traffic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimum requests and window.&lt;/strong&gt; An IP only becomes a candidate with at least this many low-score requests inside the window. The default is 20 in 15 minutes (the video used 10).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Action.&lt;/strong&gt; Log, managed challenge, JS challenge or block. I only ever used Log.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTL days.&lt;/strong&gt; How long an IP can stay in a rule without a single firewall hit before cleanup drops it. 14 days by default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Max rules.&lt;/strong&gt; A cap on how many rules the automation may own. A zone allows &lt;a href=&quot;https://developer.salesforce.com/docs/commerce/commerce-api/guide/cdn-zones-custom-rules.html&quot;&gt;500 custom rules&lt;/a&gt; in total and you have your own rules too, so leave room.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There&#39;s also a dry run switch that logs every decision without touching the zone, and an allowlist of IPs that must never be blocked.&lt;/p&gt;
&lt;h3 id=&quot;the-numbers-depend-on-your-logpush-filter&quot; tabindex=&quot;-1&quot;&gt;The numbers depend on your Logpush filter &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#the-numbers-depend-on-your-logpush-filter&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&amp;quot;20 requests in 15 minutes&amp;quot; means very different things depending on what your Logpush job ships. In the &lt;a href=&quot;https://okater.com/posts/sfcc-logpush-jobs/#drop-the-static-assets&quot;&gt;Logpush article&lt;/a&gt; I suggested keeping only &lt;code&gt;text/html&lt;/code&gt; responses. With that filter, one log line is roughly one page view or AJAX call, and 20 low-score lines is a real pattern.&lt;/p&gt;
&lt;p&gt;If the job ships everything, a single page view brings its CSS, JS, images, fonts and icons along, easily dozens of lines. Now 20 requests is less than one page load, and one shopper who happens to score low on one visit becomes a candidate. The counts also swing with page weight, so a heavy product page counts more than a light one.&lt;/p&gt;
&lt;p&gt;So decide the filter first, then pick the thresholds, and revisit them every time the filter changes.&lt;/p&gt;
&lt;p&gt;On each run the job groups the window&#39;s requests by IP, keeps the IPs that cross both thresholds, and packs them into rules named &lt;code&gt;AUTOBLOCK-001&lt;/code&gt;, &lt;code&gt;AUTOBLOCK-002&lt;/code&gt; and so on. A rule expression can be up to 4096 characters, so one rule holds a couple of hundred IPs before the next one starts. The job only touches rules with its own prefix. Your hand-made rules are read for the report and never changed.&lt;/p&gt;
&lt;p&gt;It&#39;s also declarative. Every run works out the full list of IPs that should be in rules and makes the zone match it, so if a run dies halfway, the next one fixes it.&lt;/p&gt;
&lt;p&gt;Here is the run from the video:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://okater.com/images/posts/sfcc-bot-management-automation/job-log.webp&quot; alt=&quot;Job log showing 686 files, 99 records, 7 unique IPs and 1 candidate, then creating AUTOBLOCK-001 with 2 IPs and the Log action, with the IP addresses blurred&quot; width=&quot;1600&quot; height=&quot;306&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;686 log files, 99 records inside the window, 7 unique IPs. One new candidate, plus one still remembered from an earlier run.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Each IP gets logged with its evidence: how many low-score requests, the lowest score, the ASN and the country. Later, when someone asks &amp;quot;why is this IP blocked?&amp;quot;, you have an answer. In this run one IP had 82 low-score requests with a minimum score of 3, and the other had 116 with a score of 2.&lt;/p&gt;
&lt;p&gt;And here&#39;s the result in Business Manager, a new rule at the bottom of the list with the Log action:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://okater.com/images/posts/sfcc-bot-management-automation/autoblock-rule.webp&quot; alt=&quot;Business Manager Custom Firewall Rules list with AUTOBLOCK-001 added as rule 6 with the Log action, enabled&quot; width=&quot;1600&quot; height=&quot;458&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;AUTOBLOCK-001 lands after the hand-made rules, set to Log.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id=&quot;why-ips-and-not-asns&quot; tabindex=&quot;-1&quot;&gt;Why IPs and not ASNs &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#why-ips-and-not-asns&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Blocking by ASN catches more with fewer rules, and it&#39;s much riskier. One ASN can be a whole mobile carrier, a cloud provider or a corporate network, with real shoppers mixed in with the bots. An IP has a much smaller blast radius. It&#39;s still not zero, because many people can share one IP behind a corporate network, a mobile carrier or a VPN.&lt;/p&gt;
&lt;p&gt;One more thing from the Logpush article: if another CDN sits in front of the eCDN, &lt;code&gt;ClientIP&lt;/code&gt; is that CDN&#39;s edge IP, not the shopper&#39;s. Automated IP blocking in that setup would block your own front CDN. In a stacked setup, do this at the outermost layer instead.&lt;/p&gt;
&lt;h3 id=&quot;custom-rules-or-rate-limiting-rules%3F&quot; tabindex=&quot;-1&quot;&gt;Custom rules or rate limiting rules? &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#custom-rules-or-rate-limiting-rules%3F&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The eCDN has two kinds of rules that can match an IP, and the POC uses &lt;a href=&quot;https://developer.salesforce.com/docs/commerce/commerce-api/guide/cdn-zones-custom-rules.html&quot;&gt;custom rules&lt;/a&gt;. They fit what the automation produces: a verdict. By the time an IP reaches a rule, the logs have already shown it&#39;s bot-like, so the rule only has to match it and apply an action. Custom rules give you room for that (500 per zone), every action including Log, and an order you control.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.salesforce.com/docs/commerce/commerce-api/guide/cdn-rate-limiting-rules.html&quot;&gt;Rate limiting rules&lt;/a&gt; do a different job. They count requests per IP at the edge, in real time, and act when the count goes over a limit within a period. They need no logs, but they can&#39;t see the bot score either, and a zone gets only 50 of them. So they complement the automation rather than replace it: keep a general rate limit for everyone (with Known Bot exempted), and let the automation catch the IPs that stay under it but still score like bots.&lt;/p&gt;
&lt;p&gt;There is a middle ground: instead of blocking flagged IPs, put them in a rate limiting rule with a stricter limit. A real person who scored low by bad luck never browses fast enough to hit it, but a scraper does. It&#39;s a softer landing for false positives, and it costs you one of the 50 slots.&lt;/p&gt;
&lt;h2 id=&quot;why-a-job-on-sfcc-is-not-the-real-world-shape&quot; tabindex=&quot;-1&quot;&gt;Why a job on SFCC is not the real-world shape &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#why-a-job-on-sfcc-is-not-the-real-world-shape&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The POC works, but watch the run in the video and you&#39;ll see where the time goes: downloading &lt;code&gt;.gz&lt;/code&gt; files from S3 into Commerce Cloud, unpacking them and parsing them. Most of the job is spent moving logs to where the code is. That doesn&#39;t scale with real traffic.&lt;/p&gt;
&lt;p&gt;In a real setup you flip it around and process the logs where they land. That could be an AWS Lambda function that runs whenever a new file shows up in the bucket, or a scheduled &lt;a href=&quot;https://okater.com/posts/sfcc-logpush-jobs/#so-how-do-you-actually-analyze-this%3F&quot;&gt;Athena query&lt;/a&gt; over the same files. The only thing that needs to reach Commerce Cloud is the result: a few calls to the CDN Zones API to create, update or delete rules. Your storefront instance stays out of it.&lt;/p&gt;
&lt;p&gt;The CDN Zones docs have SCAPI request examples for every call you need: create, list, update, reorder and delete for &lt;a href=&quot;https://developer.salesforce.com/docs/commerce/commerce-api/guide/cdn-zones-custom-rules.html&quot;&gt;custom rules&lt;/a&gt;, and the same set for &lt;a href=&quot;https://developer.salesforce.com/docs/commerce/commerce-api/guide/cdn-rate-limiting-rules.html&quot;&gt;rate limiting rules&lt;/a&gt;. One gotcha from building the POC: the API client&#39;s token needs two scopes, the tenant scope &lt;code&gt;SALESFORCE_COMMERCE_API:&amp;lt;tenant_id&amp;gt;&lt;/code&gt; and &lt;code&gt;sfcc.cdn-zones.rw&lt;/code&gt;. Leave out the tenant scope and the token is still issued just fine, but every call comes back 403.&lt;/p&gt;
&lt;h2 id=&quot;two-loops%3A-one-creates%2C-one-cleans-up&quot; tabindex=&quot;-1&quot;&gt;Two loops: one creates, one cleans up &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#two-loops%3A-one-creates%2C-one-cleans-up&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You only get two Logpush jobs per zone, and I suggested spending them on HTTP requests and firewall events. That split fits this setup exactly, one dataset per loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The create loop&lt;/strong&gt; reads HTTP request logs, finds new bot-like IPs and adds them to rules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The cleanup loop&lt;/strong&gt; reads firewall event logs. Every time an automated rule matches a request, a firewall event lands in the bucket with the client IP. If an IP hasn&#39;t triggered anything for N days, cleanup takes it out of the rule, and an empty rule gets deleted.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Why bother with cleanup? Bots rotate IPs, so most entries go stale within days. Without cleanup you slowly fill the 500-rule limit with addresses nobody uses anymore. And IPs get reassigned: the address a scraper used last month might belong to a real shopper today.&lt;/p&gt;
&lt;p&gt;My POC does both in one job to keep the demo short. In real life I&#39;d split them into two jobs or functions, each with one purpose, its own schedule and its own failures.&lt;/p&gt;
&lt;p&gt;Put together, every IP goes through the same lifecycle:&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
stateDiagram-v2
    [*] --&gt; Log: detected
    Log --&gt; Challenge: reviewed, no false positives
    Challenge --&gt; Block: reviewed again
    Log --&gt; Removed: quiet for N days
    Challenge --&gt; Removed: quiet for N days
    Block --&gt; Removed: quiet for N days
    Removed --&gt; [*]
&lt;/pre&gt;
&lt;h2 id=&quot;start-with-log.-always.&quot; tabindex=&quot;-1&quot;&gt;Start with Log. Always. &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#start-with-log.-always.&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Every automated rule should start life with the Log action. A Log rule matches and records, but lets the request through. You get firewall events showing exactly who the rule would have caught, and nobody is actually blocked.&lt;/p&gt;
&lt;p&gt;Leave it in Log long enough to see a normal week, and ideally a busy day like a sale or a launch. Then look at who matched. Real browsers on home connections? Your own monitoring? A payment provider&#39;s callbacks? Every one of those is a shopper or partner you would have blocked.&lt;/p&gt;
&lt;p&gt;When you move on, go to managed challenge before block. A challenge lets a real person through after a quick check, and a bot usually fails it. Block is the last step, for when you&#39;re sure.&lt;/p&gt;
&lt;p&gt;A few guardrails I&#39;d have in place before the first run:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dry run, then Log, then challenge.&lt;/strong&gt; Never straight to block.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An allowlist&lt;/strong&gt; for your monitoring and uptime checks, load tests, payment and fraud providers, integration partners and office IPs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Allow rules on top.&lt;/strong&gt; Custom rules are evaluated in order, and new rules land at the bottom. Keep your allow rules (with a skip action) above the automated ones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A kill switch.&lt;/strong&gt; Someone on call should be able to turn off every automated rule in seconds, and stop the job that would recreate them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A cap&lt;/strong&gt; on how many rules and IPs the automation can own.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An audit trail&lt;/strong&gt; that says, for every IP, why it was added and when.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-business-owns-the-cutoff&quot; tabindex=&quot;-1&quot;&gt;The business owns the cutoff &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#the-business-owns-the-cutoff&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This part isn&#39;t technical. Salesforce is clear about who does what. From the &lt;a href=&quot;https://developer.salesforce.com/docs/commerce/commerce-solutions/guide/bot-management.html&quot;&gt;Bot Management guide&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Bot protection efforts by B2C Commerce are focused primarily on maintaining the availability, stability, and performance of our multi-tenant commerce platform. [...] customers own the monitoring and tuning of the controls to protect against bots on a continuous basis.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So the controls are yours, and the thresholds are a business decision disguised as a number. A score of 30 or 50, 10 requests or 100, challenge or block: each choice trades blocked bots against blocked shoppers. Engineering can build the loop and show the data. The business has to decide how much risk is acceptable and sign off on it. And someone has to keep watching for as long as the automation runs. This isn&#39;t a set-and-forget job.&lt;/p&gt;
&lt;h2 id=&quot;before-you-build-this&quot; tabindex=&quot;-1&quot;&gt;Before you build this &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#before-you-build-this&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This POC is a cheap way to use a signal you already have. It&#39;s not the only option, and often not the first one I&#39;d reach for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rate limiting rules&lt;/strong&gt; (up to 50 per zone) catch a lot of noisy bots by request rate alone, no logs or code needed. Exempt verified crawlers with Known Bot and they cover a lot of ground.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The managed WAF rulesets&lt;/strong&gt; are already in your zone. Check their actions before you write your own rules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A dedicated bot product.&lt;/strong&gt; Behavioral detection that no log job will match. More on that next.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;third-party-bot-protection&quot; tabindex=&quot;-1&quot;&gt;Third-party bot protection &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#third-party-bot-protection&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If bots are a serious business problem for you, a dedicated product is the way to go. This slide from a bot management deck sums up the two shapes they come in:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://okater.com/images/posts/sfcc-bot-management-automation/third-party-options.webp&quot; alt=&quot;Slide titled 3rd Party BOT Prevention Solutions. Left: pre-built cartridge integrations at the origin, with HUMAN, Netacea, DataDome, Radware Bot Manager and F5 Bot Defense. Right: stacked CDN configuration at the edge, with Akamai and Cloudflare.&quot; width=&quot;1600&quot; height=&quot;820&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;Two places to stop bots: in the storefront with a cartridge, or in front of the eCDN with your own CDN.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Cartridge integrations, at the origin.&lt;/strong&gt; HUMAN, Netacea, DataDome, Radware Bot Manager and F5 Bot Defense have integrations that run as part of your storefront. Sitting in the app tier, they see all the traffic that reaches your site, whatever route it took. The catch is in the name: a bot is only stopped once it&#39;s already at your origin, so every blocked request still costs you origin resources.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A stacked CDN, at the edge.&lt;/strong&gt; Akamai Bot Manager or Cloudflare Bot Management, running in your own CDN account in front of the eCDN. Bots are stopped before they reach Commerce Cloud at all, and that&#39;s the only one of these options where you can enforce on a bot score at the edge. The price is setup and care. The eCDN now only sees your front CDN, so its own protection (WAF, firewall and rate limiting rules, geo blocking) is effectively bypassed, and &lt;code&gt;ClientIP&lt;/code&gt; in Logpush is your CDN&#39;s edge, not the shopper. Plan to follow the stacked CDN best practices closely.&lt;/p&gt;
&lt;p&gt;Either way, these are partner products, not Salesforce features, so integration support and pricing come from the vendor. They also cost real money. DataDome is one of the few that publish prices: when I recorded the video, their plans started at about $3,800 a month, and the Enterprise tier at over $13,000. To see who offers a Commerce Cloud integration, &lt;a href=&quot;https://appexchange.salesforce.com/appxSearchKeywordResults?keywords=bot&amp;amp;category=commerce&quot;&gt;search AppExchange for bot apps&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;wrapping-up&quot; tabindex=&quot;-1&quot;&gt;Wrapping up &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#wrapping-up&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The eCDN already scores every request. It just won&#39;t let you act on the score. Logpush gets the score out, a detection loop turns repeat low-score IPs into Log rules, and a cleanup loop takes them back out once they go quiet. Process the logs where they land and send only rule changes to the CDN API. And keep a person in charge: the business sets the cutoffs, every rule starts in Log, and nothing gets blocked until someone has looked.&lt;/p&gt;
&lt;p&gt;Happy bot hunting. 🤖&lt;/p&gt;
</content>
    <summary>A proof of concept that reads BotScore from eCDN Logpush logs and turns repeat low-score IPs into firewall rules. Plus the cleanup loop, the new Known Bot field, and why this needs a human in charge.</summary>
  </entry>
  <entry>
    <title>Shopper Profile Sync for SFCC and Salesforce</title>
    <link href="https://okater.com/posts/sfcc-shopper-profile-sync/"/>
    <updated>2026-10-06T00:00:00Z</updated>
    <id>https://okater.com/posts/sfcc-shopper-profile-sync/</id>
    <content type="html">&lt;p&gt;If you ever had to get B2C Commerce customers into Salesforce Core, you know it used to be a project of its own. Most of the time that project was &lt;a href=&quot;https://github.com/SalesforceCommerceCloud/b2c-crm-sync&quot;&gt;b2c-crm-sync&lt;/a&gt;, an open-source enablement framework. It was designed to teach the B2C customer data strategy, it is maintained by the community, and its README is very clear about what that means:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Please remember that this project &lt;strong&gt;should not be treated as Salesforce Product&lt;/strong&gt;. [...] Customers and partners implement this at-will with no expectation of roadmap, technical support, defect resolution, production-style SLAs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So you owned all of it: a Node CLI, SFDX deployments, OCAPI and WebDAV permissions, JWT certificates, an Account Manager client, plus the Apex, Flows and Platform Events on the Core side. And it hasn&#39;t seen an update in about three years.&lt;/p&gt;
&lt;p&gt;Now there is a native way. &lt;strong&gt;Shopper Profile Sync&lt;/strong&gt; went GA in June 2026. You connect your B2C Commerce instance to a Salesforce org from Business Manager, set up a few rules in Core, flip a switch, and registered shoppers start showing up as Person Accounts. Edit one of those Person Accounts in Core, and the change lands back in Commerce about a minute later.&lt;/p&gt;
&lt;p&gt;The Salesforce Connection you create along the way is also what Merchant Agent and Shopper Agent build on.&lt;/p&gt;
&lt;p&gt;I recorded the whole setup on a sandbox, from connecting the two clouds to watching a profile travel both ways.&lt;/p&gt;
&lt;div class=&quot;video-embed&quot;&gt;&lt;iframe src=&quot;https://customer-s2hy6afr8h81a1gk.cloudflarestream.com/41ed15c8d0a79a0dd00096400232da08/iframe?poster=https%3A%2F%2Fcustomer-s2hy6afr8h81a1gk.cloudflarestream.com%2F41ed15c8d0a79a0dd00096400232da08%2Fthumbnails%2Fthumbnail.jpg%3Ftime%3D%26height%3D600&quot; title=&quot;Setting up Shopper Profile Sync between SFCC and Salesforce&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;h2 id=&quot;shopper-profile-sync-or-unified-profile-sync%3F&quot; tabindex=&quot;-1&quot;&gt;Shopper Profile Sync or Unified Profile Sync? &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#shopper-profile-sync-or-unified-profile-sync%3F&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Same feature, two names. The documentation, the release notes and the card in Business Manager all say &lt;strong&gt;Shopper Profile Sync&lt;/strong&gt;. The feature switch says &lt;strong&gt;Enable Unified Profile Sync Feature&lt;/strong&gt;, and the system job and the Business Manager API use &amp;quot;unified profile&amp;quot; too. I&#39;ll stick with Shopper Profile Sync here, but if you are searching for answers, try both.&lt;/p&gt;
&lt;h2 id=&quot;how-it-works%3A-two-directions%2C-two-mechanisms&quot; tabindex=&quot;-1&quot;&gt;How it works: two directions, two mechanisms &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#how-it-works%3A-two-directions%2C-two-mechanisms&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&amp;quot;Two-way sync&amp;quot; sounds symmetric. It isn&#39;t, and that is the most useful thing to understand before you start.&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
flowchart LR
    S[Shopper registers&lt;br /&gt;or edits profile] --&gt; B2C[(B2C Commerce&lt;br /&gt;customer)]
    B2C --&gt;|&quot;scheduled job&lt;br /&gt;every 30 min, 10 min on ODS&quot;| PA[(Salesforce&lt;br /&gt;Person Account)]
    PA --&gt;|&quot;event, about 1 min&quot;| B2C
    A[Service agent&lt;br /&gt;edits record] --&gt; PA
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Commerce to Salesforce is a scheduled job.&lt;/strong&gt; A system job picks up new and changed profiles and pushes them to Salesforce. It runs every 30 minutes in production and every 10 minutes on on-demand sandboxes, up to 100,000 records per run. That cadence arrived in 26.9. Before that it was hourly, so if you read older threads complaining about the wait, that&#39;s why.&lt;/p&gt;
&lt;p&gt;You don&#39;t have to guess when the next run is. The Shopper Profile Sync card under &lt;strong&gt;Administration → Global Preferences → Salesforce Connection&lt;/strong&gt; shows the last sync status and the next scheduled sync:&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;https://okater.com/images/posts/sfcc-shopper-profile-sync/bm-salesforce-connection.webp&quot; alt=&quot;Business Manager Salesforce Connection page with an active connection and the Shopper Profile Sync card showing Last Sync Status: Synced and the next scheduled sync time&quot; width=&quot;1640&quot; height=&quot;940&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;The card tells you exactly how long you&#39;ll be staring at the Accounts list.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Salesforce to Commerce is event-driven.&lt;/strong&gt; When someone edits a synced Person Account in Core, the change is pushed to Commerce right away. The docs say about a minute. In the video I renamed a shopper in Core, refreshed the storefront, and the new name was already there.&lt;/p&gt;
&lt;h3 id=&quot;the-fine-print-behind-%22two-way%22&quot; tabindex=&quot;-1&quot;&gt;The fine print behind &amp;quot;two-way&amp;quot; &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#the-fine-print-behind-%22two-way%22&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Person Accounts are only created from the Commerce side.&lt;/strong&gt; If you create a Person Account by hand in Core, no B2C customer appears.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Updates go both ways, but only for linked records.&lt;/strong&gt; That means shoppers who went through the sync from Commerce first, or who got matched to an existing Person Account by your duplicate rules (more on that below).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only registered shoppers sync.&lt;/strong&gt; Guest checkouts don&#39;t create Person Accounts through this feature.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deletes don&#39;t sync.&lt;/strong&gt; Deleting a customer in Commerce leaves the Person Account in place. Deleting a linked Person Account in Core doesn&#39;t delete the customer either. The link is dropped, and the next sync creates the Person Account again. If you are cleaning test data out of a sandbox org, turn off Auto-Sync first, or the records come back.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is one more nuance for new registrations. Since 26.8, a fresh registration can create the Person Account in near real time instead of waiting for the next job run, on headless storefronts (PWA Kit, Storefront Next). My sandbox runs SFRA, and the new account waited for the job, which is what you see in the video.&lt;/p&gt;
&lt;h2 id=&quot;before-you-start&quot; tabindex=&quot;-1&quot;&gt;Before you start &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#before-you-start&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A B2C Commerce sandbox on a recent release.&lt;/strong&gt; The feature moved fast between 26.6 and 26.9, so the newer the better.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A Salesforce org to pair with.&lt;/strong&gt; For testing, use a Salesforce sandbox or a demo org, not production. The connection is one to one and Person Accounts can&#39;t be switched off, so experiments belong somewhere you can throw away. For the real setup, the org needs Enterprise or Unlimited edition with Data 360 provisioned.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Salesforce Connection page in Business Manager.&lt;/strong&gt; If &lt;strong&gt;Administration → Global Preferences → Salesforce Connection&lt;/strong&gt; isn&#39;t there, check &lt;strong&gt;Feature Switches&lt;/strong&gt; for &lt;strong&gt;Unified Commerce Configuration&lt;/strong&gt;. If the page is there but the Shopper Profile Sync card isn&#39;t, enable &lt;strong&gt;Enable Unified Profile Sync Feature&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A look at your Change Data Capture usage.&lt;/strong&gt; Core reports synced records back to Commerce through Account and Contact change events. If your org already has Change Data Capture on for Contact, setup shows a warning: a large first sync can produce a lot of change events and eat into your daily event allocation. It doesn&#39;t block anything, but check your limits before you sync a few million shoppers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pick your pairing.&lt;/strong&gt; One B2C Commerce instance connects to exactly one Salesforce org, and you can&#39;t split it per site. If you run several country sites on one realm, they all land in the same org.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-setup-checklist&quot; tabindex=&quot;-1&quot;&gt;The setup checklist &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#the-setup-checklist&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This follows the official &lt;a href=&quot;https://help.salesforce.com/s/articleView?id=cc.b2c_configure_shopper_profile_sync.htm&amp;amp;type=5&quot;&gt;Configure Shopper Profile Sync&lt;/a&gt; guide, in the order that actually works, with the bits that confused me spelled out. Tick the boxes as you go (they don&#39;t survive a page refresh, so keep the tab open).&lt;/p&gt;
&lt;ul class=&quot;checklist&quot;&gt;
  &lt;li&gt;
    &lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;&lt;a href=&quot;https://help.salesforce.com/s/articleView?id=cc.b2c_connected_comm_salesforce_connection.htm&amp;amp;type=5&quot;&gt;Connect your B2C Commerce instance to Salesforce&lt;/a&gt;. In Business Manager go to &lt;strong&gt;Administration → Global Preferences → Salesforce Connection&lt;/strong&gt; and hit &lt;strong&gt;Connect&lt;/strong&gt;.&lt;/span&gt;&lt;/label&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;The login opens on &lt;code&gt;test.salesforce.com&lt;/code&gt;. That&#39;s right for a Salesforce sandbox. For a production or developer org, change &lt;code&gt;test&lt;/code&gt; to &lt;code&gt;login&lt;/code&gt; in the address bar before you log in, otherwise it won&#39;t work.&lt;/span&gt;&lt;/label&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;&lt;a href=&quot;https://help.salesforce.com/s/articleView?id=sales.account_person_enable.htm&amp;amp;type=5&quot;&gt;Enable Person Accounts&lt;/a&gt; in Salesforce Setup. Many demo orgs already have them on.&lt;/span&gt;&lt;/label&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;Heads up: once Person Accounts are enabled, they can&#39;t be turned off. No big deal in a demo org, a real decision in a production one.&lt;/span&gt;&lt;/label&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;&lt;a href=&quot;https://help.salesforce.com/s/articleView?id=cc.b2c_set_up_person_account_duplicate_rules_for_shopper_profile_sync.htm&amp;amp;type=5&quot;&gt;Set up Person Account duplicate rules for Shopper Profile Sync&lt;/a&gt;. Matching rules first, then duplicate rules.&lt;/span&gt;&lt;/label&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;Create the &lt;strong&gt;3 matching rules&lt;/strong&gt; on the Person Account object, using the names and criteria from the docs. For example, the registered shopper rule matches on &lt;strong&gt;Commerce Customer Reference&lt;/strong&gt; and &lt;strong&gt;Commerce Organization Reference&lt;/strong&gt;, both &lt;strong&gt;Exact&lt;/strong&gt;, &lt;strong&gt;Match Blank&lt;/strong&gt; off, filter logic &lt;code&gt;1 AND 2&lt;/code&gt;. Save and activate each one.&lt;/span&gt;&lt;/label&gt;&lt;/li&gt;
      &lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;Create the &lt;strong&gt;3 duplicate rules&lt;/strong&gt;, one per matching rule: &lt;strong&gt;Bypass sharing rules&lt;/strong&gt;, action on create and on edit set to &lt;strong&gt;Allow&lt;/strong&gt;, &lt;strong&gt;Alert&lt;/strong&gt; unchecked, &lt;strong&gt;Report&lt;/strong&gt; checked. Attach the matching rule, add the condition from the docs (for the registered shopper rule that&#39;s &lt;strong&gt;Commerce Customer Reference&lt;/strong&gt; not equal to blank), then save and activate.&lt;/span&gt;&lt;/label&gt;&lt;/li&gt;
      &lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;Double-check &lt;strong&gt;Report&lt;/strong&gt;. The docs say &quot;no alert, report&quot;, which is easy to misread as &quot;no report&quot;. If Report is off, the rule won&#39;t activate.&lt;/span&gt;&lt;/label&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;Back in Business Manager, hit &lt;strong&gt;Get Started&lt;/strong&gt; on the Shopper Profile Sync card and review the attribute mappings. The defaults are fine unless you have extra data to sync.&lt;/span&gt;&lt;/label&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;If you do: the standard fields come mapped, and you can add up to 20 custom ones. Only simple types are supported (Text, Boolean, Integer, Long, Double), and a Salesforce field has to be visible to the integration user to show up in the list.&lt;/span&gt;&lt;/label&gt;&lt;/li&gt;
      &lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;Mappings are one to one, and the data types on both sides have to match. Map everything you need now: you can&#39;t edit mappings while Auto-Sync is on, so changing them later means switching it off first.&lt;/span&gt;&lt;/label&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;Turn on &lt;strong&gt;Auto-Sync&lt;/strong&gt;. Do this last, not earlier. Without active rules and complete mappings you get &lt;em&gt;&quot;Unable to start synchronization. Please map all required fields and configure duplicate rules&quot;&lt;/em&gt;, and the status sits at &lt;strong&gt;Initialization Pending&lt;/strong&gt;.&lt;/span&gt;&lt;/label&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;Know what you&#39;re starting. The first time you turn it on, it runs a full sync of every registered shopper on the instance, then switches to the scheduled runs for changes only. Turning it off pauses those runs.&lt;/span&gt;&lt;/label&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;Test &lt;strong&gt;Commerce → Salesforce&lt;/strong&gt;: register a new customer on the storefront, wait for the next scheduled sync on the card, then check &lt;strong&gt;Accounts → All Person Accounts&lt;/strong&gt; in Salesforce.&lt;/span&gt;&lt;/label&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;label&gt;&lt;input type=&quot;checkbox&quot; /&gt; &lt;span&gt;Test &lt;strong&gt;Salesforce → Commerce&lt;/strong&gt;: edit that Person Account in Salesforce (I added &quot;SF Core&quot; to the first and last name), then refresh the storefront account page or the customer in &lt;strong&gt;Merchant Tools → Customers&lt;/strong&gt;. Give it about a minute.&lt;/span&gt;&lt;/label&gt;
  &lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why-three-duplicate-rules%3F&quot; tabindex=&quot;-1&quot;&gt;Why three duplicate rules? &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#why-three-duplicate-rules%3F&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Setting up six rules by hand feels like a lot for &amp;quot;sync my customers&amp;quot;, so it helps to know what they are for. The recommended set covers three situations: a registered shopper, a guest shopper, and a registered shopper claiming an earlier guest record.&lt;/p&gt;
&lt;p&gt;That last one is the interesting case. A shopper checks out as a guest, and Order Management creates a Person Account for that order. A month later the same person registers on your storefront. Without the claim rule you end up with two Person Accounts for one human. With it, the sync finds the guest record, links it to the new shopper and updates it instead.&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
flowchart TD
    R[Registered shopper&lt;br /&gt;arrives from Commerce] --&gt; M{Duplicate rules&lt;br /&gt;find a match?}
    M --&gt;|&quot;Already linked&quot;| U[Update that&lt;br /&gt;Person Account]
    M --&gt;|&quot;Guest record from&lt;br /&gt;an earlier order&quot;| C[Claim it and link&lt;br /&gt;to the shopper]
    M --&gt;|&quot;No match&quot;| N[Create a new&lt;br /&gt;Person Account]
&lt;/pre&gt;
&lt;p&gt;If your org already has Person Accounts from somewhere else, these rules are also what stops the first sync from flooding it with duplicates. Worth testing on a sandbox with realistic data before production.&lt;/p&gt;
&lt;h2 id=&quot;what-about-b2c-crm-sync%3F&quot; tabindex=&quot;-1&quot;&gt;What about b2c-crm-sync? &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#what-about-b2c-crm-sync%3F&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you are starting fresh, go native. If you already run b2c-crm-sync, it is worth looking at what you actually use, because the two don&#39;t cover exactly the same ground:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;b2c-crm-sync&lt;/th&gt;
&lt;th&gt;Shopper Profile Sync&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Support&lt;/td&gt;
&lt;td&gt;Community project, no official support&lt;/td&gt;
&lt;td&gt;Salesforce feature, supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;Node CLI, SFDX, OCAPI and WebDAV permissions, JWT certificates, Account Manager client&lt;/td&gt;
&lt;td&gt;Connect in Business Manager, rules in Core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2C Commerce API&lt;/td&gt;
&lt;td&gt;OCAPI Shop and Data APIs (deprecated)&lt;/td&gt;
&lt;td&gt;Built in, nothing to configure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer model&lt;/td&gt;
&lt;td&gt;Person Accounts or Accounts and Contacts&lt;/td&gt;
&lt;td&gt;Person Accounts only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extras&lt;/td&gt;
&lt;td&gt;Order on Behalf, agent password reset, address books, promotions view&lt;/td&gt;
&lt;td&gt;Profile fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Topology&lt;/td&gt;
&lt;td&gt;Multiple instances, customer lists and sites&lt;/td&gt;
&lt;td&gt;One instance to one org&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance&lt;/td&gt;
&lt;td&gt;Yours&lt;/td&gt;
&lt;td&gt;Salesforce&#39;s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;There is one more reason to move: OCAPI. b2c-crm-sync talks to B2C Commerce through the OCAPI Shop API and Data API, and OCAPI was marked deprecated in April 2026. It still gets support and security fixes, but no new features, and SCAPI is where everything new lands. The README promises a port to the new APIs &amp;quot;in a future release&amp;quot;, and with the project quiet for three years, I wouldn&#39;t wait for it.&lt;/p&gt;
&lt;p&gt;If you only use b2c-crm-sync to get profiles into Core, Shopper Profile Sync replaces it with a lot less to maintain. There is no switch that migrates you, though. The move is a data job, and it&#39;s the next section.&lt;/p&gt;
&lt;h2 id=&quot;moving-from-b2c-crm-sync&quot; tabindex=&quot;-1&quot;&gt;Moving from b2c-crm-sync &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#moving-from-b2c-crm-sync&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The two tools decide who owns a record by looking at different fields. b2c-crm-sync manages the Person Accounts that have its &lt;code&gt;B2C_*&lt;/code&gt; ID fields filled in. Shopper Profile Sync manages the ones that have its Commerce reference fields filled in. So the move comes down to copying the IDs from the old fields into the new ones and blanking the old ones. Ownership changes hands one shopper at a time.&lt;/p&gt;
&lt;p&gt;Why copy at all, instead of letting the first sync figure it out? Because of the registered shopper matching rule. It matches on Commerce Customer Reference and Commerce Organization Reference, exact. If those are already filled in when you turn on Auto-Sync, the first full sync finds every existing Person Account and updates it. Skip the copy, and you&#39;re counting on the broad email rule to find them, while b2c-crm-sync still thinks they&#39;re its own. Two integrations writing to the same record is not a fun afternoon.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;b2c-crm-sync field (on the Person Account)&lt;/th&gt;
&lt;th&gt;Shopper Profile Sync field&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;B2C_Customer_ID__pc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Commerce Customer Reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;B2C_Customer_No__pc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Account Number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;B2C_CustomerList_ID__pc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Commerce Group Reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;None, it&#39;s a constant: your realm and instance, as &lt;code&gt;&amp;lt;realm&amp;gt;_&amp;lt;instance&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Commerce Organization Reference&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Note the &lt;code&gt;__pc&lt;/code&gt;. b2c-crm-sync defines these fields on Contact, so on a Person Account they show up with the &lt;code&gt;__pc&lt;/code&gt; suffix, and that&#39;s what you select in Data Loader. &lt;code&gt;B2C_Site_ID__pc&lt;/code&gt; and the &lt;code&gt;B2C_CustomerList__c&lt;/code&gt; lookup have no counterpart. Keep them or clear them, the new sync doesn&#39;t care.&lt;/p&gt;
&lt;p&gt;The order I&#39;d follow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Rehearse on a sandbox&lt;/strong&gt; with a copy of real data. Not production, not even &amp;quot;just to see&amp;quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Switch b2c-crm-sync off on both sides.&lt;/strong&gt; In B2C Commerce, turn off the &lt;code&gt;b2ccrm_syncIsEnabled&lt;/code&gt; site preference or take &lt;code&gt;int_b2ccrmsync&lt;/code&gt; off the cartridge path. In Core, deactivate its B2C Instance and B2C Customer List records so its flows stop calling out.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Get temporary edit access to the Commerce reference fields.&lt;/strong&gt; They are system-managed and read-only, even for admins with field-level edit access. Shopper Profile Sync comes with a system permission for exactly this. Search the system permissions for &amp;quot;Shopper Profile Sync&amp;quot; and add it to your user through a permission set or your profile.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run the data job.&lt;/strong&gt; Export the Person Accounts where &lt;code&gt;B2C_Customer_ID__pc&lt;/code&gt; isn&#39;t blank, only for the customer list of the instance you&#39;re pairing. Fill in the new columns, blank the old ones and run an update with Data Loader using the Bulk API. Keep the export file. It&#39;s your undo button.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remove the temporary permission&lt;/strong&gt;, so the fields go back to being system-only.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set up Shopper Profile Sync&lt;/strong&gt; with the checklist above and turn on Auto-Sync last. Compare the Person Account count before and after the first sync. It should grow by roughly the number of shoppers b2c-crm-sync never synced, not double.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clean up later:&lt;/strong&gt; the Account Manager API client, the OCAPI settings, the connected app, and the &lt;code&gt;b2ccrm_*&lt;/code&gt; custom attributes on customer profiles.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What you give up: b2c-crm-sync&#39;s quick actions on the Contact (Order on Behalf, agent password reset, the active promotions view) stop working once you switch it off in step 2, and they need the customer ID you just blanked anyway. Shopper Profile Sync has no equivalent in Core. Agents can still shop on behalf from Customer Service Center in Business Manager, so check it covers your flows before step 4. And two setups have no clean path at all. If you use the Accounts and Contacts model, Shopper Profile Sync only works with Person Accounts. If you sync several instances into one org, only the shoppers of the paired instance move over.&lt;/p&gt;
&lt;h2 id=&quot;when-it-doesn&#39;t-sync&quot; tabindex=&quot;-1&quot;&gt;When it doesn&#39;t sync &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#when-it-doesn&#39;t-sync&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A few places to look, roughly in the order I&#39;d check them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The card itself.&lt;/strong&gt; Last sync status and next scheduled sync answer most &amp;quot;where is my customer?&amp;quot; questions. Usually the answer is &amp;quot;the job hasn&#39;t run yet&amp;quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Initialization Pending.&lt;/strong&gt; Almost always missing or inactive duplicate rules, or required mappings that aren&#39;t complete.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Some shoppers sync, some never do.&lt;/strong&gt; Look at your Person Account validation rules and required fields. A validation rule that a Commerce profile can&#39;t satisfy (a mandatory phone number, say) rejects that shopper on every run. A required custom field needs a mapping or a default value.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No Shopper Profile Sync card.&lt;/strong&gt; It&#39;s the feature switch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The system job.&lt;/strong&gt; It&#39;s called &lt;code&gt;sfcc-sync-unified-shopper-profiles&lt;/code&gt; and shows up as a system job in Business Manager, so depending on your permissions you may not see it at all.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;wrapping-up&quot; tabindex=&quot;-1&quot;&gt;Wrapping up &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#wrapping-up&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Connect the two clouds, enable Person Accounts, set up the three matching rules and the three duplicate rules (with Report on), turn on Auto-Sync last, and you have a supported two-way profile sync without a single line of custom code. Just remember the asymmetry: Commerce creates and syncs on a schedule, Salesforce only updates and does it almost instantly.&lt;/p&gt;
&lt;p&gt;Next up is Merchant Agent, which uses the very same Salesforce Connection. Happy syncing. 🔄&lt;/p&gt;
</content>
    <summary>How to set up Shopper Profile Sync between B2C Commerce and a Salesforce org. A video walkthrough, a checklist you can follow along with, how each sync direction actually works, and the fine print behind &quot;two-way&quot;.</summary>
  </entry>
  <entry>
    <title>Logpush Jobs for SFCC eCDN: All you need to know</title>
    <link href="https://okater.com/posts/sfcc-logpush-jobs/"/>
    <updated>2026-09-16T00:00:00Z</updated>
    <id>https://okater.com/posts/sfcc-logpush-jobs/</id>
    <content type="html">&lt;p&gt;If you want to know what is actually happening to your traffic on SFCC, the eCDN logs are where the truth lives. Who hit your site, from which ASN, with what bot score, which firewall rule fired. You can analyze it after the fact, or you can act on it. The hot topic right now is telling bot traffic apart from real shoppers, and these logs are the raw material for exactly that. You can even automate firewall rules based on them, but that is a whole other article: &lt;a href=&quot;https://okater.com/posts/sfcc-bot-management-automation/&quot;&gt;Bot Management Automation for SFCC eCDN with Logpush&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Today let&#39;s keep it simple: how to get those logs out of the eCDN and into an S3 bucket with a Logpush job.&lt;/p&gt;
&lt;p&gt;I recorded a full walkthrough where I set one up on a sandbox using a free-tier AWS account. Setup to first log in under 16 minutes.&lt;/p&gt;
&lt;div class=&quot;video-embed&quot;&gt;&lt;iframe src=&quot;https://customer-s2hy6afr8h81a1gk.cloudflarestream.com/580e890d84dc748edce7c32227a6dfda/iframe?poster=https%3A%2F%2Fcustomer-s2hy6afr8h81a1gk.cloudflarestream.com%2F580e890d84dc748edce7c32227a6dfda%2Fthumbnails%2Fthumbnail.jpg%3Ftime%3D%26height%3D600&quot; title=&quot;Setting up a Logpush job for SFCC eCDN&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;h2 id=&quot;the-good-news%3A-there-is-finally-a-ui&quot; tabindex=&quot;-1&quot;&gt;The good news: there is finally a UI &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#the-good-news%3A-there-is-finally-a-ui&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For a long time Logpush jobs could only be created through the API. As of a release or two ago, there is now a proper UI for it inside Business Manager, under &lt;strong&gt;Administration → Embedded CDN → Settings&lt;/strong&gt;. On top of that, eCDN is now available on sandboxes, which means you can safely test all of this on a sandbox before you touch anything that matters.&lt;/p&gt;
&lt;p&gt;Here is the shape of what we are building:&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
flowchart LR
    U[Shopper] --&gt; E[SFCC eCDN&lt;br /&gt;Cloudflare]
    E --&gt; O[SFCC Origin]
    E -. logs .-&gt; LP[Logpush job]
    LP --&gt; S3[(Amazon S3)]
&lt;/pre&gt;
&lt;p&gt;The eCDN sees every request, and Logpush quietly ships a copy of the log lines to your bucket. That is the whole idea.&lt;/p&gt;
&lt;h2 id=&quot;first-rule-of-logpush%3A-you-only-get-two-jobs&quot; tabindex=&quot;-1&quot;&gt;First rule of Logpush: you only get two jobs &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#first-rule-of-logpush%3A-you-only-get-two-jobs&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This is the constraint that shapes everything else, so let&#39;s get it out of the way early. &lt;strong&gt;You get a maximum of two Logpush jobs per zone.&lt;/strong&gt; Two. That&#39;s it.&lt;/p&gt;
&lt;p&gt;So spend them wisely. The split that makes the most sense:&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
flowchart TB
    Z[Your zone] --&gt; J1[Job 1&lt;br /&gt;HTTP requests]
    Z --&gt; J2[Job 2&lt;br /&gt;Firewall events]
    J1 --&gt; A1[General traffic analysis&lt;br /&gt;bot scores, ASNs, paths]
    J2 --&gt; A2[Which firewall rules actually fire]
&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Job 1 for HTTP requests.&lt;/strong&gt; This is your general traffic feed. Bot scores, client IPs, ASNs, paths, user agents, the works.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Job 2 for firewall events.&lt;/strong&gt; This tells you which of your firewall rules are actually being triggered and what action was taken. That matters more than it sounds, because there is a hard limit of 500 firewall rules. A firewall-events job lets you see which rules earn their keep, so you can prune the ones that never fire and free up room.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alright, let&#39;s build one.&lt;/p&gt;
&lt;h2 id=&quot;creating-the-job-(the-happy-path)&quot; tabindex=&quot;-1&quot;&gt;Creating the job (the happy path) &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#creating-the-job-(the-happy-path)&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;New Logpush job. Pick your &lt;strong&gt;destination&lt;/strong&gt; (I&#39;m using Amazon S3, but pick whatever fits your stack). Then &lt;strong&gt;select the dataset&lt;/strong&gt;: HTTP requests or Firewall events. We&#39;ll start with HTTP requests.&lt;/p&gt;
&lt;p&gt;Point it at your bucket and give it a path. Small tip that saves future-you a headache: use the &lt;code&gt;{DATE}&lt;/code&gt; placeholder in the path. Logpush creates a &lt;em&gt;lot&lt;/em&gt; of files, and dumping them all into one flat folder gets messy fast. Something like &lt;code&gt;logs/{DATE}&lt;/code&gt; keeps each day tidy.&lt;/p&gt;
&lt;p&gt;Set the &lt;strong&gt;region&lt;/strong&gt; to match your S3 bucket exactly (mine is Ohio). If these do not match, nothing works.&lt;/p&gt;
&lt;h2 id=&quot;the-two-gotchas-that-will-get-you&quot; tabindex=&quot;-1&quot;&gt;The two gotchas that will get you &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#the-two-gotchas-that-will-get-you&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Everything above is smooth. These two are where people (me included) lose ten minutes.&lt;/p&gt;
&lt;h3 id=&quot;gotcha-1%3A-the-ownership-challenge-hates-the-date-placeholder&quot; tabindex=&quot;-1&quot;&gt;Gotcha 1: the ownership challenge hates the date placeholder &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#gotcha-1%3A-the-ownership-challenge-hates-the-date-placeholder&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The UI hands you an AWS resource policy. Copy it, go to your bucket → &lt;strong&gt;Permissions → Bucket policy&lt;/strong&gt;, and paste it in. It saves cleanly, no errors, looks great.&lt;/p&gt;
&lt;p&gt;Then you hit &lt;strong&gt;Next&lt;/strong&gt; and get &lt;strong&gt;&amp;quot;Failed to send ownership challenge.&amp;quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The reason is that the S3 side does not accept the &lt;code&gt;{DATE}&lt;/code&gt; placeholder during the ownership challenge step. Fix: edit the path, remove the placeholder, and try again. This time the challenge goes through and drops a small challenge file into your bucket. Open it, copy the token, paste it back into the UI, confirm, and you own the destination.&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
flowchart LR
    A[Paste bucket policy] --&gt; B{Hit Next}
    B --&gt;|&quot;{DATE} in path&quot;| C[Failed to send&lt;br /&gt;ownership challenge]
    C --&gt; D[Remove placeholder&lt;br /&gt;retry]
    D --&gt; E[Grab token&lt;br /&gt;from bucket]
    E --&gt; F[Confirm ownership]
&lt;/pre&gt;
&lt;h3 id=&quot;gotcha-2%3A-your-test-traffic-never-reaches-the-ecdn&quot; tabindex=&quot;-1&quot;&gt;Gotcha 2: your test traffic never reaches the eCDN &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#gotcha-2%3A-your-test-traffic-never-reaches-the-ecdn&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;You create the job, it says &lt;strong&gt;pending&lt;/strong&gt;, and now you want to see it flip to active. The only way is to actually push some logs through, which means generating real traffic.&lt;/p&gt;
&lt;p&gt;So you open the storefront and browse around. And nothing happens.&lt;/p&gt;
&lt;p&gt;Here is why: hitting the normal storefront URL goes &lt;strong&gt;straight to origin&lt;/strong&gt; and skips the eCDN entirely. Open dev tools, look at the response headers, and you&#39;ll see no Cloudflare headers at all. No eCDN, no logs.&lt;/p&gt;
&lt;p&gt;For sandboxes you need to browse through the &lt;code&gt;my.&lt;/code&gt; host (the &lt;code&gt;*.my.commercecloud.salesforce.com&lt;/code&gt; one). That request actually routes through the eCDN, and &lt;em&gt;now&lt;/em&gt; your logs start flowing. Browse a few pages and give it a minute.&lt;/p&gt;
&lt;h2 id=&quot;filters%3A-store-what-you-need%2C-not-everything&quot; tabindex=&quot;-1&quot;&gt;Filters: store what you need, not everything &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#filters%3A-store-what-you-need%2C-not-everything&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When you name the job you get to choose &lt;strong&gt;all logs&lt;/strong&gt; or &lt;strong&gt;filtered logs&lt;/strong&gt;. Filter. Please filter.&lt;/p&gt;
&lt;p&gt;Storing everything is overwhelming and eats space fast. Filtering lets you keep only what you care about. In the demo I filtered on two fields:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ClientRequestHost&lt;/code&gt; &lt;strong&gt;contains&lt;/strong&gt; my sandbox host&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ClientRequestPath&lt;/code&gt; &lt;strong&gt;contains&lt;/strong&gt; &lt;code&gt;/s/RefArch&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;drop-the-static-assets&quot; tabindex=&quot;-1&quot;&gt;Drop the static assets &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#drop-the-static-assets&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Here is the filter that saves you the most, and the one I&#39;d add to almost every HTTP-requests job: &lt;strong&gt;throw away the static content.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Think about what you actually want from these logs — page visits, who&#39;s hitting what, bot scores, ASNs. You do &lt;em&gt;not&lt;/em&gt; care that a shopper&#39;s browser also pulled down forty CSS, JS, image, and font files to render that one page. But the eCDN logs every one of them. In practice static assets are the overwhelming majority of your log lines, which means most of your storage (and most of the noise) is spent on requests you&#39;ll never look at.&lt;/p&gt;
&lt;p&gt;The cleanest way to cut them in one shot is to filter on the response content type:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EdgeResponseContentType&lt;/code&gt; &lt;strong&gt;contains&lt;/strong&gt; &lt;code&gt;text/html&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This keeps full page loads &lt;em&gt;and&lt;/em&gt; the AJAX controller fragments that make up a real session — the &lt;code&gt;Cart-MiniCartShow&lt;/code&gt; record above is &lt;code&gt;text/html&lt;/code&gt;, so it survives — while dropping CSS, JS, images, and fonts, none of which carry that type. A &lt;code&gt;BotScore&lt;/code&gt; on a &lt;code&gt;.woff2&lt;/code&gt; tells you nothing the parent page request didn&#39;t already, so you lose no signal.&lt;/p&gt;
&lt;p&gt;Two things to keep in mind before you commit to it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It also drops JSON responses&lt;/strong&gt; (SCAPI/OCAPI and JSON controller endpoints). That&#39;s fine for a storefront-traffic view. If you&#39;re specifically hunting API abuse, don&#39;t use this filter — or widen it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It drops redirects and typeless responses&lt;/strong&gt; too. Minor, but worth knowing if 3xx chains matter to you.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You also pick which &lt;strong&gt;fields&lt;/strong&gt; to store. Grab the ones that actually help you reason about traffic and skip the noise. My default set:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ClientASN&lt;/code&gt;, &lt;code&gt;ClientIP&lt;/code&gt;, &lt;code&gt;ClientIPClass&lt;/code&gt; (who and from where)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ClientRequestURI&lt;/code&gt;, &lt;code&gt;ClientRequestUserAgent&lt;/code&gt; (what and with what)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BotScore&lt;/code&gt; (the good stuff, more below)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RayID&lt;/code&gt; (so you can correlate with everything else)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I leave &lt;code&gt;Cookies&lt;/code&gt; off. You almost never want that sitting in a bucket.&lt;/p&gt;
&lt;h2 id=&quot;what-a-log-line-looks-like&quot; tabindex=&quot;-1&quot;&gt;What a log line looks like &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#what-a-log-line-looks-like&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Logs land compressed as &lt;code&gt;.gz&lt;/code&gt;. Unzip one and each line is a JSON object. There are a lot of fields, but only a handful do the heavy lifting for traffic analysis. Here is a trimmed record with the ones that matter most highlighted:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;highlight-line&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;mark class=&quot;highlight-line highlight-line-active&quot;&gt;  &lt;span class=&quot;token property&quot;&gt;&quot;BotScore&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;98&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/mark&gt;
&lt;span class=&quot;highlight-line&quot;&gt;  &lt;span class=&quot;token property&quot;&gt;&quot;BotScoreSrc&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Machine Learning&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;mark class=&quot;highlight-line highlight-line-active&quot;&gt;  &lt;span class=&quot;token property&quot;&gt;&quot;ClientASN&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;14340&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/mark&gt;
&lt;mark class=&quot;highlight-line highlight-line-active&quot;&gt;  &lt;span class=&quot;token property&quot;&gt;&quot;ClientCountry&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;us&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/mark&gt;
&lt;mark class=&quot;highlight-line highlight-line-active&quot;&gt;  &lt;span class=&quot;token property&quot;&gt;&quot;ClientIP&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;123.123.123.123&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/mark&gt;
&lt;span class=&quot;highlight-line&quot;&gt;  &lt;span class=&quot;token property&quot;&gt;&quot;ClientIPClass&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;noRecord&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;highlight-line&quot;&gt;  &lt;span class=&quot;token property&quot;&gt;&quot;ClientRequestHost&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;abcd-123.my.commercecloud.salesforce.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;mark class=&quot;highlight-line highlight-line-active&quot;&gt;  &lt;span class=&quot;token property&quot;&gt;&quot;ClientRequestURI&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/on/demandware.store/Sites-RefArch-Site/en_US/Cart-MiniCartShow&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/mark&gt;
&lt;span class=&quot;highlight-line&quot;&gt;  &lt;span class=&quot;token property&quot;&gt;&quot;ClientRequestUserAgent&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ... Chrome/152.0.0.0 Safari/537.36&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;highlight-line&quot;&gt;  &lt;span class=&quot;token property&quot;&gt;&quot;EdgeResponseStatus&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;highlight-line&quot;&gt;  &lt;span class=&quot;token property&quot;&gt;&quot;SecurityActions&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;log&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;highlight-line&quot;&gt;  &lt;span class=&quot;token property&quot;&gt;&quot;SecurityRuleDescription&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Log Rule&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;highlight-line&quot;&gt;  &lt;span class=&quot;token property&quot;&gt;&quot;RayID&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;a3caa7730c129682&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;highlight-line&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;details class=&quot;code-toggle&quot;&gt;
&lt;summary&gt;Show the full record — every field the eCDN captured&lt;/summary&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;BotDetectionIDs&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;BotScore&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;98&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;BotScoreSrc&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Machine Learning&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;BotTags&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;CacheCacheStatus&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dynamic&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;CacheReserveUsed&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;CacheResponseBytes&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4606&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientASN&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;14340&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientCountry&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;us&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientDeviceType&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientIP&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;123.123.123.123&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientIPClass&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;noRecord&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientRequestBytes&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7798&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientRequestHost&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;abcd-123.my.commercecloud.salesforce.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientRequestMethod&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;GET&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientRequestPath&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/on/demandware.store/Sites-RefArch-Site/en_US/Cart-MiniCartShow&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientRequestProtocol&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;HTTP/2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientRequestReferer&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://abcd-123.my.commercecloud.salesforce.com/s/RefArch/checked-silk-tie-25752235M.html?lang=en_US&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientRequestScheme&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientRequestSource&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;eyeball&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientRequestURI&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/on/demandware.store/Sites-RefArch-Site/en_US/Cart-MiniCartShow&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientRequestUserAgent&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientSSLCipher&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ECDHE-ECDSA-AES128-GCM-SHA256&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientSSLProtocol&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;TLSv1.2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ClientTCPRTTMs&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;Cookies&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgeCFConnectingO2O&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgeColoCode&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;IAD&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgeColoID&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgeEndTimestamp&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2026-09-17T19:52:55Z&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgePathingOp&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;wl&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgePathingSrc&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;undef&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgePathingStatus&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;nr&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgeRequestHost&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;abcd-123.my.commercecloud.salesforce.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgeResponseBodyBytes&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1758&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgeResponseBytes&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2636&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgeResponseCompressionRatio&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4.4920363&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgeResponseContentType&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;text/html;charset=UTF-8&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgeResponseStatus&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgeServerIP&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;104.23.209.15&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgeStartTimestamp&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2026-09-17T19:52:55Z&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;EdgeTimeToFirstByteMs&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;85&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;OriginRequestHeaderSendDurationMs&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;OriginResponseDurationMs&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;74&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;OriginResponseHeaderReceiveDurationMs&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;73&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;OriginResponseStatus&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;OriginTCPHandshakeDurationMs&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;OriginTLSHandshakeDurationMs&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ParentRayID&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;00&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;RayID&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;a3caa7730c129682&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;SecurityActions&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;log&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;SecurityRuleDescription&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Log Rule&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;SecurityRuleIDs&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;b93e6d91300e4784b024c4181axxyyzz&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;SecuritySources&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;firewallCustom&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ZoneName&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;abcd-123.my.commercecloud.salesforce.com&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;The star of the show is &lt;code&gt;BotScore&lt;/code&gt;. Cloudflare scores traffic from &lt;strong&gt;1 to 99&lt;/strong&gt;. Close to 99 means almost certainly a human. Close to 1 means almost certainly a bot. The record above sits at 98, so, real person. Pair the score with &lt;code&gt;ClientASN&lt;/code&gt;, &lt;code&gt;ClientCountry&lt;/code&gt;, and &lt;code&gt;ClientIP&lt;/code&gt; and you have the ingredients for automated firewall rules that block the obvious bots. That is the &amp;quot;act on your logs&amp;quot; part I keep teasing.&lt;/p&gt;
&lt;p&gt;If you also run the firewall-events job, each record tells you the &lt;code&gt;Action&lt;/code&gt; taken, the &lt;code&gt;RuleID&lt;/code&gt;, and the rule &lt;code&gt;Description&lt;/code&gt;, so you can watch your rules do their job in real time.&lt;/p&gt;
&lt;h2 id=&quot;so-how-do-you-actually-analyze-this%3F&quot; tabindex=&quot;-1&quot;&gt;So how do you actually analyze this? &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#so-how-do-you-actually-analyze-this%3F&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;One record is easy to read. You will never look at just one. A busy storefront produces millions of lines a day, and even filtered down to &lt;code&gt;text/html&lt;/code&gt; a single day is hundreds of thousands of records spread across a pile of &lt;code&gt;.gz&lt;/code&gt; files. Opening them by hand is not a plan.&lt;/p&gt;
&lt;p&gt;The mental model that fixes this: &lt;strong&gt;S3 is a bucket, not a tool.&lt;/strong&gt; It holds your gzipped JSON and does nothing else. To get answers out of it, you point a query engine at it.&lt;/p&gt;
&lt;p&gt;Since the logs already live in S3, the natural choice is &lt;strong&gt;Amazon Athena&lt;/strong&gt;. It runs plain SQL directly against the files in your bucket — no server to manage, you pay per query. Remember the &lt;code&gt;{DATE}&lt;/code&gt; placeholder from earlier? This is where it pays off: partition the table by date and each query scans only the days you ask about instead of the whole bucket, which keeps it fast and cheap. (While you&#39;re there, add an S3 lifecycle rule to expire old logs so the bucket doesn&#39;t grow forever.)&lt;/p&gt;
&lt;p&gt;And because the goal is spotting bots, one query does what no human could eyeball — group a day&#39;s traffic by ASN and surface the networks that are busy &lt;em&gt;and&lt;/em&gt; score badly:&lt;/p&gt;
&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; ClientASN&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
       &lt;span class=&quot;token function&quot;&gt;COUNT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;      &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; hits&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
       &lt;span class=&quot;token function&quot;&gt;AVG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;BotScore&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; avg_bot_score
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt;   ecdn_http_requests
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt;  &lt;span class=&quot;token keyword&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;2026-09-18&#39;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;GROUP&lt;/span&gt;  &lt;span class=&quot;token keyword&quot;&gt;BY&lt;/span&gt; ClientASN
&lt;span class=&quot;token keyword&quot;&gt;HAVING&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;AVG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;BotScore&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;30&lt;/span&gt;     &lt;span class=&quot;token comment&quot;&gt;-- consistently bot-like&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;ORDER&lt;/span&gt;  &lt;span class=&quot;token keyword&quot;&gt;BY&lt;/span&gt; hits &lt;span class=&quot;token keyword&quot;&gt;DESC&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That list — noisy networks with low bot scores — is exactly the input for the automated firewall rules I keep promising.&lt;/p&gt;
&lt;p&gt;Want charts instead of a SQL prompt? Put &lt;strong&gt;Grafana&lt;/strong&gt; on top of Athena. Grafana draws the dashboards; Athena still does the querying underneath. You keep one copy of your data in S3 and get the visual view for free.&lt;/p&gt;
&lt;p&gt;One shortcut worth knowing: if your organization already runs &lt;strong&gt;Splunk&lt;/strong&gt; or &lt;strong&gt;Datadog&lt;/strong&gt;, Logpush can push straight to them and you get their analysis views and dashboards out of the box — you may not need S3 at all. Worth it only if you&#39;re already paying for one; standing one up just for eCDN logs is overkill.&lt;/p&gt;
&lt;p&gt;Whatever you pick, the shape is the same: &lt;strong&gt;cheap storage in S3, a query engine on top.&lt;/strong&gt; Start with Athena, add Grafana if you want pictures, and reach for Splunk or Datadog only when they&#39;re already in the building.&lt;/p&gt;
&lt;h2 id=&quot;one-thing-nobody-warns-you-about%3A-client-ip-and-stacked-cdns&quot; tabindex=&quot;-1&quot;&gt;One thing nobody warns you about: client IP and stacked CDNs &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#one-thing-nobody-warns-you-about%3A-client-ip-and-stacked-cdns&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here is a subtle one worth knowing before you build anything on top of these logs.&lt;/p&gt;
&lt;p&gt;The Logpush schema is a &lt;strong&gt;fixed list of fields&lt;/strong&gt;. There is no field for arbitrary request headers, and &lt;code&gt;ClientIP&lt;/code&gt; is always the IP that &lt;em&gt;connected to the eCDN&lt;/em&gt;. Normally that is your shopper, and life is good.&lt;/p&gt;
&lt;p&gt;But if you put another CDN in front of the eCDN (a stacked setup, say Fastly or Akamai out front), the eCDN only ever sees the &lt;em&gt;front CDN&#39;s&lt;/em&gt; edge IP. So &lt;code&gt;ClientIP&lt;/code&gt; in your Logpush output becomes that edge IP, not the real shopper.&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
flowchart LR
    S[Real shopper&lt;br /&gt;1.2.3.4] --&gt; F[Front CDN&lt;br /&gt;Fastly / Akamai]
    F --&gt;|&quot;eCDN sees&lt;br /&gt;the front CDN IP&quot;| E[SFCC eCDN]
    E --&gt; O[Origin]
    E -. Logpush .-&gt; L[(ClientIP = front CDN,&lt;br /&gt;not 1.2.3.4)]
&lt;/pre&gt;
&lt;p&gt;And because the schema has no slot for a custom header, you &lt;strong&gt;cannot&lt;/strong&gt; recover the real shopper IP from eCDN Logpush, no matter how you name headers upstream. Anything that depends on the true client IP (IP-based rate limiting, geo blocking, bot score) gets fuzzy in a stacked setup, because the eCDN is scoring the front CDN, not the shopper.&lt;/p&gt;
&lt;p&gt;The practical takeaway: if you run a stacked CDN, capture real client IPs at the &lt;strong&gt;outermost layer&lt;/strong&gt; (the one that actually sees the shopper first). Keep the eCDN logs for what the eCDN genuinely observed: rule hits, paths, RayIDs, and edge behavior. They are still great for that.&lt;/p&gt;
&lt;h2 id=&quot;wrapping-up&quot; tabindex=&quot;-1&quot;&gt;Wrapping up &lt;a class=&quot;header-anchor&quot; href=&quot;https://okater.com/#wrapping-up&quot; aria-hidden=&quot;true&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;That is the whole thing. Two jobs per zone, one for HTTP requests and one for firewall events, filtered down to what you care about, landing in S3, with a query engine like Athena on top to make sense of it. Watch out for the date placeholder during the ownership challenge, and remember to send your test traffic through the &lt;code&gt;my.&lt;/code&gt; host so it actually hits the eCDN.&lt;/p&gt;
&lt;p&gt;Happy logging. 📦&lt;/p&gt;
</content>
    <summary>How to ship your SFCC eCDN traffic logs to S3 using the new Business Manager UI. A video walkthrough, the filters worth setting, the gotchas that will bite you, how to actually analyze the pile, and one thing about client IPs nobody warns you about.</summary>
  </entry>
  <entry>
    <title>SFCC Vibe Coding with an MCP Server</title>
    <link href="https://okater.com/posts/sfcc-vibe-coding-mcp-server/"/>
    <updated>2025-12-11T00:00:00Z</updated>
    <id>https://okater.com/posts/sfcc-vibe-coding-mcp-server/</id>
    <content type="html">&lt;p&gt;At this point, it is hard to imagine SFCC development without using AI. It speeds things up significantly. Of course, you need to be careful with it, which is a separate &amp;quot;Vibe Coding&amp;quot; topic.&lt;/p&gt;
&lt;p&gt;The specific challenge with SFCC Vibe Coding is that standard LLMs usually aren&#39;t very good with SFCC context.&lt;/p&gt;
&lt;p&gt;To cover that gap, we can use an MCP Server. This allows the AI to actually understand our environment. 📖 &lt;a href=&quot;https://www.linkedin.com/pulse/taming-ai-beast-custom-toolkit-supercharge-copilot-commerce-theunen-0i7se/&quot;&gt;Thomas Theunen describes this concept perfectly here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I created a video guide on how to configure this and a demo showing Copilot automatically fetching error logs to resolve issues.&lt;/p&gt;
&lt;div class=&quot;video-embed&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/cSs0u9b_vhk&quot; title=&quot;SFCC Vibe Coding with an MCP Server&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;p&gt;MCP Server: &lt;a href=&quot;https://sfcc-mcp-dev.rhino-inquisitor.com/&quot;&gt;https://sfcc-mcp-dev.rhino-inquisitor.com&lt;/a&gt; (Creator: Thomas Theunen)&lt;/p&gt;
&lt;p&gt;There are some security concerns:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;dw.json&lt;/strong&gt; — I&#39;d suggest creating a dedicated SFCC account/credentials with a limited access to sandboxes only, maybe DEV, but definitely not STG/PRD. It might be one shared account per project/team.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OCAPI Client&lt;/strong&gt; — Same here, a dedicated API Client per project with a limited access to sandboxes (and maybe DEV) only.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3rd-party MCP Server&lt;/strong&gt; — While MCP Server from Thomas sounds reliable, it is still 3rd-party with unexpected updates. And enterprise security models can be against it. In this case we can clone it (since it&#39;s under MIT).&lt;/li&gt;
&lt;/ol&gt;
</content>
    <summary>Using an MCP Server to give AI coding assistants real SFCC context — a video walkthrough, and the security tradeoffs worth knowing before you wire one up.</summary>
  </entry>
</feed>
