{"id":24,"date":"2025-12-06T19:06:48","date_gmt":"2025-12-06T19:06:48","guid":{"rendered":"https:\/\/balamurali.in\/blog\/?p=24"},"modified":"2026-02-23T14:26:41","modified_gmt":"2026-02-23T14:26:41","slug":"building-a-chrome-extension-to-debug-analytics-sdks-a-deep-dive","status":"publish","type":"post","link":"https:\/\/balamurali.in\/blog\/learn-with-me\/building-a-chrome-extension-to-debug-analytics-sdks-a-deep-dive\/","title":{"rendered":"Building a Chrome Extension to Debug Analytics SDKs"},"content":{"rendered":"\n<p>If you&#8217;ve ever worked with analytics SDKs, you know the pain. Someone reports that form submissions aren&#8217;t being tracked. Or user identification calls are mysteriously failing. You open Chrome DevTools, navigate to the Network tab, try to filter requests, and&#8230; you&#8217;re hunting through hundreds of API calls, most of which are encoded in some proprietary format.<\/p>\n\n\n\n<p>At <a href=\"https:\/\/factors.ai\" target=\"_blank\" rel=\"noreferrer noopener\">Factors<\/a>, I faced this problem constantly while helping customers debug their SDK implementations. The questions were always the same:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>&#8220;Is the SDK even loading?&#8221;<\/li>\n\n\n\n<li>&#8220;Is it loading via our website or through Google Tag Manager?&#8221;<\/li>\n\n\n\n<li>&#8220;Are the network calls going through?&#8221;<\/li>\n\n\n\n<li>&#8220;What data is actually being sent?&#8221;<\/li>\n\n\n\n<li>&#8220;Why aren&#8217;t our forms being tracked?&#8221;<\/li>\n<\/ul>\n\n\n\n<p>I needed a tool that could answer all of these instantly. So I built one.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What I Built<\/h2>\n\n\n\n<p>The <strong>Factors SDK Debugger<\/strong> is a Chrome extension that:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Detects SDK presence<\/strong> \u2013 Not just &#8220;is it there?&#8221; but <em>how<\/em> it is installed (manual HTML, Google Tag Manager, or both)<\/li>\n\n\n\n<li><strong>Intercepts ALL network requests<\/strong> \u2013 Including <code>fetch()<\/code>, <code>XMLHttpRequest<\/code>, and <code>sendBeacon()<\/code><\/li>\n\n\n\n<li><strong>Decodes encrypted payloads<\/strong> \u2013 The SDK uses Caesar cipher encoding; the extension decodes everything automatically<\/li>\n\n\n\n<li><strong>Tracks forms in real-time<\/strong> \u2013 Scans the DOM, including iframes, to show which forms are being tracked<\/li>\n\n\n\n<li><strong>Persists data across navigations<\/strong> \u2013 Data stays available even as users navigate around a site<\/li>\n<\/ol>\n\n\n\n<p>But the interesting part isn&#8217;t <em>what<\/em> it does\u2014it&#8217;s <em>how<\/em> it does it.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Technical Deep Dive<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Challenge #1: Running Code in the Page&#8217;s JavaScript Context<\/h3>\n\n\n\n<p>Chrome extensions run in an &#8220;isolated world&#8221; by default\u2014a separate JavaScript context from the page. This is great for security but terrible for intercepting network calls that the page makes.<\/p>\n\n\n\n<p>The solution? Chrome&#8217;s Manifest V3 introduced <code>world: \"MAIN\"<\/code> for content scripts:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"content_scripts\": &#91;\n    {\n      \"matches\": &#91;\"&amp;lt;all_urls&amp;gt;\"],\n      \"js\": &#91;\"content\/interceptor.js\"],\n      \"run_at\": \"document_start\",\n      \"world\": \"MAIN\",  \/\/ Run in page context, not isolated world\n      \"all_frames\": true\n    }\n  ]\n}\n<\/code><\/pre>\n\n\n\n<p>This lets my interceptor script monkey-patch <code>fetch<\/code>, <code>XMLHttpRequest<\/code>, and <code>navigator.sendBeacon<\/code> <strong>before any page scripts run<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Challenge #2: Capturing Requests Before They Leave<\/h3>\n\n\n\n<p>Here&#8217;s how I intercept <code>sendBeacon<\/code> (commonly used by analytics SDKs):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const originalBeacon = navigator.sendBeacon.bind(navigator);\n\nnavigator.sendBeacon = function (url, data) {\n  if (isFactorsUrl(url)) {\n    \/\/ Capture the request data before it's sent\n    sendNetworkRequest({\n      url: url,\n      method: 'POST',\n      requestBody: data,\n      timestamp: Date.now(),\n      type: 'beacon',\n      pageUrl: window.location.href\n    });\n  }\n\n  return originalBeacon(url, data);\n};\n<\/code><\/pre>\n\n\n\n<p>The key insight: I capture the data <strong>before<\/strong> calling the original function. This means I get the full request body, which Chrome DevTools&#8217; Network tab often can&#8217;t show for <code>sendBeacon<\/code> calls.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Challenge #3: Dual-Layer Capture for 100% Coverage<\/h3>\n\n\n\n<p>What if my content script injection fails? What if there&#8217;s a race condition? I built a <strong>dual-layer approach<\/strong>:<\/p>\n\n\n\n<p><strong>Layer 1: Content Script Injection (Primary)<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Runs in <code>MAIN<\/code> world at <code>document_start<\/code><\/li>\n\n\n\n<li>Intercepts network APIs directly<\/li>\n\n\n\n<li>Gets full request bodies<\/li>\n<\/ul>\n\n\n\n<p><strong>Layer 2: Service Worker with webRequest API (Backup)<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Uses Chrome&#8217;s <code>webRequest.onBeforeRequest<\/code><\/li>\n\n\n\n<li>Catches any requests that slip through<\/li>\n\n\n\n<li>Provides deduplication to prevent double-counting<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Service worker backup layer\nchrome.webRequest.onBeforeRequest.addListener(\n  async (details) =&amp;gt; {\n    if (!isFactorsCall(details.url)) return;\n\n    const timestamp = Date.now();\n\n    \/\/ Check if content script already captured this\n    if (isRequestDuplicate(details.url, timestamp, 'webRequest')) {\n      return; \/\/ Skip - content script got it\n    }\n\n    \/\/ Store the request...\n  },\n  { urls: &#91;'&amp;lt;all_urls&amp;gt;'] },\n  &#91;'requestBody']\n);\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Challenge #4: Decoding Obfuscated Payloads<\/h3>\n\n\n\n<p>The Factors SDK encodes all request bodies using a Caesar cipher (<code>shift = 4<\/code>). Instead of making users decode this manually, the extension does it automatically:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function decodeFactorsPayload(str, shift = 4) {\n  if (!str || typeof str !== 'string') return str;\n\n  let decoded = '';\n  const first = 33 + shift; \/\/ 37\n\n  for (let i = 0; i &amp;lt; str.length; i++) {\n    const charCode = str&#91;i].charCodeAt(0);\n\n    if (charCode &gt;= first &amp;amp;&amp;amp; charCode &amp;lt;= 126) {\n      decoded += String.fromCharCode(charCode - shift);\n    } else if (charCode &amp;lt; first &amp;amp;&amp;amp; charCode &gt;= 33) {\n      \/\/ Handle wrap-around\n      decoded += String.fromCharCode((charCode % 33) + (126 - shift) + 1);\n    } else {\n      decoded += str&#91;i];\n    }\n  }\n\n  return decoded;\n}\n<\/code><\/pre>\n\n\n\n<p>Now users see clean JSON payloads instead of gibberish.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Challenge #5: Detecting Manual vs GTM Implementation<\/h3>\n\n\n\n<p>This was surprisingly tricky. The SDK creates a <code>&lt;script&gt;<\/code> element dynamically, so you can&#8217;t just check for <code>&lt;script src=\"factors.js\"&gt;<\/code> in the HTML.<\/p>\n\n\n\n<p>My approach:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Fetch the raw HTML source<\/strong> with XHR (not the DOM, which includes dynamic scripts)<\/li>\n\n\n\n<li><strong>Search for the SDK stub pattern<\/strong> in the raw HTML<\/li>\n\n\n\n<li><strong>If found in raw HTML<\/strong> \u2192 Manual implementation<\/li>\n\n\n\n<li><strong>If only in DOM (not raw HTML)<\/strong> \u2192 Dynamic\/GTM implementation<\/li>\n\n\n\n<li><strong>If both<\/strong> \u2192 Flag as duplicate installation (often a bug!)<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>const SDK_PATTERNS = {\n  \/\/ The initialization pattern (always present in the stub)\n  faitrackerInit: \/window\\.faitracker\\s*=\\s*window\\.faitracker\\s*\\|\\|\/i,\n  \/\/ Constants that confirm it's the actual stub\n  faitrackerConst: \/FAITRACKER_QUEUED_EVENT|FAITRACKER_INIT_EVENT\/i\n};\n\nfunction checkRawHtmlSource() {\n  const xhr = new XMLHttpRequest();\n  xhr.open('GET', window.location.href, true);\n  xhr.onreadystatechange = function () {\n    if (xhr.readyState === 4 &amp;amp;&amp;amp; xhr.status === 200) {\n      const html = xhr.responseText;\n      const hasFactorsInSource =\n        SDK_PATTERNS.faitrackerInit.test(html) &amp;amp;&amp;amp;\n        SDK_PATTERNS.faitrackerConst.test(html);\n\n      if (hasFactorsInSource) {\n        \/\/ It's a manual implementation\n      }\n    }\n  };\n  xhr.send();\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Challenge #6: Real-Time Form Detection with Iframe Support<\/h3>\n\n\n\n<p>Tracking forms is more complex than you&#8217;d think. Forms can be:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Static in the HTML<\/li>\n\n\n\n<li>Dynamically added by JavaScript<\/li>\n\n\n\n<li>Inside same-origin iframes (like embedded HubSpot forms)<\/li>\n\n\n\n<li>Modified after initial load<\/li>\n<\/ul>\n\n\n\n<p>My solution polls the DOM every 5 seconds, extracting forms from both the main document and accessible iframes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function extractFormsFromDocument(doc, sourceLabel) {\n  const forms = &#91;];\n  const formElements = doc.querySelectorAll('form');\n\n  formElements.forEach((form, index) =&amp;gt; {\n    forms.push({\n      id: form.id || null,\n      name: form.getAttribute('name') || null,\n      action: form.getAttribute('action') || null,\n      method: (form.getAttribute('method') || 'GET').toUpperCase(),\n      source: sourceLabel,\n      index\n    });\n  });\n\n  return forms;\n}\n\nfunction detectAllForms() {\n  const detectedForms = &#91;];\n\n  \/\/ Extract forms from current document\n  const currentDocForms = extractFormsFromDocument(document, 'main');\n  detectedForms.push(...currentDocForms);\n\n  \/\/ Check inside same-origin iframes\n  const iframes = document.querySelectorAll('iframe');\n  iframes.forEach((iframe, index) =&amp;gt; {\n    try {\n      const iframeDoc = iframe.contentDocument;\n      if (iframeDoc) {\n        const iframeForms = extractFormsFromDocument(iframeDoc, `iframe-${index}`);\n        detectedForms.push(...iframeForms);\n      }\n    } catch (e) {\n      \/\/ Cross-origin iframe - can't access\n    }\n  });\n\n  return detectedForms;\n}\n<\/code><\/pre>\n\n\n\n<p>The extension also checks for specific tracking attributes (<code>data-faitracker-form-bind<\/code>, <code>data-faitracker-input-id<\/code>) to show users exactly which forms and fields the SDK is monitoring.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lessons Learned<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1. Race Conditions Are Everywhere<\/h3>\n\n\n\n<p>Network interceptors, content scripts, service workers\u2014they all run at different times. I implemented:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Request buffering<\/strong> with sequence numbers<\/li>\n\n\n\n<li><strong>Debounced batch sending<\/strong> (50ms window)<\/li>\n\n\n\n<li><strong>Deduplication<\/strong> using URL + timestamp as keys<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">2. Storage Requires Locking<\/h3>\n\n\n\n<p>Multiple requests can trigger storage updates simultaneously. I built a simple async lock:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const storageLocks = new Map();\n\nasync function withStorageLock(domain, operation) {\n  while (storageLocks.has(domain)) {\n    await storageLocks.get(domain);\n  }\n\n  let releaseLock;\n  const lockPromise = new Promise((resolve) =&amp;gt; {\n    releaseLock = resolve;\n  });\n\n  storageLocks.set(domain, lockPromise);\n\n  try {\n    return await operation();\n  } finally {\n    storageLocks.delete(domain);\n    releaseLock();\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">3. The Extension Must Be Invisible Until Needed<\/h3>\n\n\n\n<p>Users don&#8217;t want to think about debugging tools until something breaks. The extension:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Shows a checkmark badge when the SDK is detected<\/li>\n\n\n\n<li>Automatically cleans up data after 24 hours<\/li>\n\n\n\n<li>Groups network calls by page URL for easy navigation<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Architecture<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502                         Browser Page                            \u2502\n\u2502                                                                 \u2502\n\u2502  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510                  \u2502\n\u2502  \u2502  interceptor.js \u2502    \u2502  content-script.js \u2502                  \u2502\n\u2502  \u2502  (MAIN world)   \u2502\u2500\u2500\u2500\u25b6\u2502  (Isolated world)  \u2502                  \u2502\n\u2502  \u2502                 \u2502    \u2502                    \u2502                  \u2502\n\u2502  \u2502 \u2022 Patches fetch \u2502    \u2502 \u2022 Bridges messages \u2502                  \u2502\n\u2502  \u2502 \u2022 Patches XHR   \u2502    \u2502 \u2022 Buffers requests \u2502                  \u2502\n\u2502  \u2502 \u2022 Patches beacon\u2502    \u2502 \u2022 Detects forms    \u2502                  \u2502\n\u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518                  \u2502\n\u2502                                   \u2502                              \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                    \u2502\n                                    \u25bc\n              \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n              \u2502          service-worker.js              \u2502\n              \u2502                                         \u2502\n              \u2502  \u2022 webRequest backup capture            \u2502\n              \u2502  \u2022 Storage management                   \u2502\n              \u2502  \u2022 Payload decoding                     \u2502\n              \u2502  \u2022 Data persistence (24h)               \u2502\n              \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                    \u2502\n                                    \u25bc\n              \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n              \u2502              popup.js                   \u2502\n              \u2502                                         \u2502\n              \u2502  \u2022 Tabbed UI (Overview\/Events\/Network)  \u2502\n              \u2502  \u2022 Real-time updates                    \u2502\n              \u2502  \u2022 Decoded payload display              \u2502\n              \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What I&#8217;d Do Differently<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Use TypeScript<\/strong> \u2013 The codebase grew larger than expected. Type safety would have caught several bugs earlier.<\/li>\n\n\n\n<li><strong>Add unit tests<\/strong> \u2013 The encoding\/decoding logic and deduplication logic are perfect candidates for testing.<\/li>\n\n\n\n<li><strong>Build a &#8220;record session&#8221; feature<\/strong> \u2013 Export all captured data for sharing with team members.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Building this extension taught me that browser extensions are surprisingly powerful\u2014and surprisingly complex. The ability to intercept network requests before they&#8217;re sent, run code in the page&#8217;s context, and persist data across navigations makes Chrome extensions an underrated platform for developer tools.<\/p>\n\n\n\n<p>If you&#8217;re building debugging tools, consider the dual-layer approach: inject code directly where possible, but always have a backup. And never assume your code runs first\u2014race conditions will find you.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><em>The extension is internal to Factors, but the patterns here apply to any analytics SDK debugger. Have questions about the implementation? Feel free to reach out!<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>If you&#8217;ve ever worked with analytics SDKs, you know the pain. Someone reports that form submissions aren&#8217;t being tracked. Or user identification calls are mysteriously failing. You open Chrome DevTools,&#8230;<\/p>\n","protected":false},"author":1,"featured_media":151,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[8],"tags":[],"class_list":["post-24","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-learn-with-me"],"jetpack_featured_media_url":"https:\/\/balamurali.in\/blog\/wp-content\/uploads\/2026\/02\/building_chrome_extension.png","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/posts\/24","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/comments?post=24"}],"version-history":[{"count":2,"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/posts\/24\/revisions"}],"predecessor-version":[{"id":29,"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/posts\/24\/revisions\/29"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/media\/151"}],"wp:attachment":[{"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/media?parent=24"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/categories?post=24"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/balamurali.in\/blog\/wp-json\/wp\/v2\/tags?post=24"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}