| Server IP : 35.80.110.71 / Your IP : 216.73.216.221 Web Server : Apache/2.4.58 (Ubuntu) System : Linux ip-172-31-21-44 6.17.0-1019-aws #19~24.04.1-Ubuntu SMP Tue Jun 23 18:53:06 UTC 2026 x86_64 User : ubuntu ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/splitstream-web/.next/server/app/ |
Upload File : |
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/chunks/002zte3riz0h6.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/2xx73w8k5o69w.js"/><script src="/_next/static/chunks/0_bmh1s0g_5m3.js" async=""></script><script src="/_next/static/chunks/390e59xl7grtx.js" async=""></script><script src="/_next/static/chunks/3ss6b_eo0gpqh.js" async=""></script><script src="/_next/static/chunks/turbopack-0frl60onblppy.js" async=""></script><script src="/_next/static/chunks/3u-4v10_yuvs8.js" async=""></script><script src="/_next/static/chunks/31jk64s9j6agx.js" async=""></script><link rel="preload" href="https://www.googletagmanager.com/gtag/js?id=G-HZ8Z4XP611" as="script"/><title>With Pennant — Splitstream</title><meta name="description" content="Production-shaped A/B testing platform: sticky bucketing, mutex experiment groups, Sample Ratio Mismatch detection, guardrail metrics, an Octane carve-out for the hot path, and a Bayesian stats engine whose decision rule is empirically calibrated — not asserted."/><script src="/_next/static/chunks/0cz1d0mv5g_q7.js" noModule=""></script></head><body><div hidden=""><!--$--><!--/$--></div><header class="border-b border-(--color-paper-dim) bg-(--color-paper)"><div class="mx-auto flex max-w-6xl items-center justify-between px-6 py-5"><a class="text-lg font-bold tracking-tight text-(--color-ink) no-underline" href="/">Splitstream<span class="ml-2 rounded-full bg-(--color-accent-soft) px-2 py-0.5 text-xs font-medium text-(--color-accent) align-middle">portfolio demo</span></a><nav class="hidden md:flex items-center gap-5 text-sm text-(--color-ink-dim)"><a href="/docs">Docs</a><a href="/reference">API reference</a><a href="/sdks">SDKs</a><a href="/bucketing">Bucketing</a><a href="/stats-playground">Stats playground</a><a href="/with-pennant">With Pennant</a><a href="/about">About</a></nav><button type="button" aria-label="Open menu" aria-expanded="false" aria-controls="site-mobile-nav" class="md:hidden inline-flex items-center justify-center rounded-md border border-(--color-paper-dim) p-2 text-(--color-ink) cursor-pointer"><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg></button></div></header><main class="mx-auto max-w-6xl px-6 py-10"><article class="prose"><h1 class="text-3xl font-bold">Splitstream + Pennant — the joint stack</h1><p><a href="https://pennant.philiprehberger.com">Pennant</a> is a feature-flag / remote-config service with real-time SDK push. Splitstream is an A/B testing platform. The natural pair:</p><ul><li><strong>Pennant decides whether the feature ships.</strong>Use the flag as the kill switch — if it's off, no variant traffic.</li><li><strong>Splitstream decides which variant the user sees.</strong> <!-- -->And whether the variant moved the metric.</li></ul><p>The<!-- --> <a href="/sdks">@philiprehberger/growth</a> umbrella wraps both clients in a single <code>useExperiment()</code>:</p><pre><code>import { Pennant } from "@philiprehberger/pennant";
import { Splitstream } from "@philiprehberger/splitstream";
import { Growth } from "@philiprehberger/growth";
const growth = new Growth({
pennant: new Pennant({
apiKey: process.env.PENNANT_KEY!,
context: { userId: "alice" },
}),
splitstream: new Splitstream({
apiKey: process.env.SPLITSTREAM_KEY!,
context: { userId: "alice" },
}),
});
const { variant, killSwitchActive, track } = await growth.useExperiment(
"checkout-v2",
{ fallback: "control" }
);
if (killSwitchActive) {
// Pennant flag was off — Splitstream was NOT consulted.
// Exposure was NOT logged. Returns 'control' (or your controlKey).
} else {
// Pennant flag was on — Splitstream.assign() ran, exposure logged.
// variant ∈ {'control', 'treatment', null}.
}
track("purchase-completed", { revenue: 49 });</code></pre><h2>What the umbrella does (and doesn't)</h2><ul><li><strong>Does:</strong> read the Pennant flag with the same key shape (<code>${experimentKey}-enabled</code> by default), gate the Splitstream call accordingly, expose a unified<!-- --> <code>track()</code> callback. Total wrapper LOC: ~150.</li><li><strong>Doesn't:</strong>own state, persist anything, or replace either client's features. Both work standalone.</li></ul><h2>Override the kill-switch convention</h2><pre><code>new Growth({
pennant,
splitstream,
killSwitchKeyFor: (experimentKey) => `exp.${experimentKey}.gate`,
killSwitchDefault: false, // fail closed when the flag isn't in the snapshot
});</code></pre><h2>React</h2><pre><code>import { GrowthProvider, useExperiment } from "@philiprehberger/growth/react";
function App() {
return (
<GrowthProvider growth={growth}>
<CheckoutButton />
</GrowthProvider>
);
}
function CheckoutButton() {
const { variant, killSwitchActive, isLoading, track } = useExperiment("checkout-v2", {
fallback: "control",
});
if (isLoading) return <button disabled>Loading…</button>;
return (
<button
className={variant === "treatment" ? "bg-green-500" : "bg-blue-500"}
onClick={() => track("purchase-completed", { revenue: 49 })}
>
Buy now
</button>
);
}</code></pre><h2>Why ship them as separate products</h2><p>Same buyer overlap, different artifacts. Pennant's headliner is the SDK + SSE story. Splitstream's headliner is the assignment API + the credible-interval readout. Sharing a repo would muddle both pitches. They cross-link from the docs sites, and the joint sample is the same five lines on both. The umbrella exists so the joint pitch can be one call.</p></article><!--$--><!--/$--></main><footer class="border-t border-(--color-paper-dim) mt-20"><div class="mx-auto flex max-w-6xl items-center justify-between px-6 py-8 text-sm text-(--color-ink-dim)"><div>Splitstream is a portfolio demonstration by<!-- --> <a href="https://philiprehberger.com">Philip Rehberger</a>.</div><div class="flex flex-wrap gap-4"><a href="https://github.com/philiprehberger/splitstream">GitHub</a><a href="/pricing">Pricing</a><a href="/downloads">Downloads</a><a href="/status">Status</a><a href="/about">About this demo</a></div></div></footer><script src="/_next/static/chunks/2xx73w8k5o69w.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[79404,[\"/_next/static/chunks/3u-4v10_yuvs8.js\",\"/_next/static/chunks/31jk64s9j6agx.js\"],\"default\"]\n3:I[88287,[\"/_next/static/chunks/3u-4v10_yuvs8.js\",\"/_next/static/chunks/31jk64s9j6agx.js\"],\"default\"]\n4:I[74895,[\"/_next/static/chunks/3u-4v10_yuvs8.js\",\"/_next/static/chunks/31jk64s9j6agx.js\"],\"default\"]\n5:I[4561,[\"/_next/static/chunks/3u-4v10_yuvs8.js\",\"/_next/static/chunks/31jk64s9j6agx.js\"],\"\"]\n6:I[70805,[\"/_next/static/chunks/3u-4v10_yuvs8.js\",\"/_next/static/chunks/31jk64s9j6agx.js\"],\"default\"]\n12:I[52576,[\"/_next/static/chunks/3u-4v10_yuvs8.js\",\"/_next/static/chunks/31jk64s9j6agx.js\"],\"default\",1]\n:HL[\"/_next/static/chunks/002zte3riz0h6.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"c\":[\"\",\"with-pennant\"],\"q\":\"\",\"i\":false,\"f\":[[[\"\",{\"children\":[\"with-pennant\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",16],[[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/002zte3riz0h6.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/chunks/3u-4v10_yuvs8.js\",\"async\":true,\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-1\",{\"src\":\"/_next/static/chunks/31jk64s9j6agx.js\",\"async\":true,\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"children\":[[\"$\",\"$L2\",null,{}],[\"$\",\"main\",null,{\"className\":\"mx-auto max-w-6xl px-6 py-10\",\"children\":[\"$\",\"$L3\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L4\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}],[\"$\",\"footer\",null,{\"className\":\"border-t border-(--color-paper-dim) mt-20\",\"children\":[\"$\",\"div\",null,{\"className\":\"mx-auto flex max-w-6xl items-center justify-between px-6 py-8 text-sm text-(--color-ink-dim)\",\"children\":[[\"$\",\"div\",null,{\"children\":[\"Splitstream is a portfolio demonstration by\",\" \",[\"$\",\"a\",null,{\"href\":\"https://philiprehberger.com\",\"children\":\"Philip Rehberger\"}],\".\"]}],[\"$\",\"div\",null,{\"className\":\"flex flex-wrap gap-4\",\"children\":[[\"$\",\"a\",null,{\"href\":\"https://github.com/philiprehberger/splitstream\",\"children\":\"GitHub\"}],[\"$\",\"$L5\",null,{\"href\":\"/pricing\",\"children\":\"Pricing\"}],[\"$\",\"$L5\",null,{\"href\":\"/downloads\",\"children\":\"Downloads\"}],[\"$\",\"$L5\",null,{\"href\":\"/status\",\"children\":\"Status\"}],[\"$\",\"$L5\",null,{\"href\":\"/about\",\"children\":\"About this demo\"}]]}]]}]}],[\"$\",\"$L6\",null,{}]]}]}]]}],{\"children\":[[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L3\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L4\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"article\",null,{\"className\":\"prose\",\"children\":[[\"$\",\"h1\",null,{\"className\":\"text-3xl font-bold\",\"children\":\"Splitstream + Pennant — the joint stack\"}],[\"$\",\"p\",null,{\"children\":[[\"$\",\"a\",null,{\"href\":\"https://pennant.philiprehberger.com\",\"children\":\"Pennant\"}],\" is a feature-flag / remote-config service with real-time SDK push. Splitstream is an A/B testing platform. The natural pair:\"]}],[\"$\",\"ul\",null,{\"children\":[[\"$\",\"li\",null,{\"children\":[[\"$\",\"strong\",null,{\"children\":\"Pennant decides whether the feature ships.\"}],\"Use the flag as the kill switch — if it's off, no variant traffic.\"]}],[\"$\",\"li\",null,{\"children\":[[\"$\",\"strong\",null,{\"children\":\"Splitstream decides which variant the user sees.\"}],\" \",\"And whether the variant moved the metric.\"]}]]}],[\"$\",\"p\",null,{\"children\":[\"The\",\" \",[\"$\",\"$L5\",null,{\"href\":\"/sdks\",\"children\":\"@philiprehberger/growth\"}],\" umbrella wraps both clients in a single \",[\"$\",\"code\",null,{\"children\":\"useExperiment()\"}],\":\"]}],[\"$\",\"pre\",null,{\"children\":[\"$\",\"code\",null,{\"children\":\"import { Pennant } from \\\"@philiprehberger/pennant\\\";\\nimport { Splitstream } from \\\"@philiprehberger/splitstream\\\";\\nimport { Growth } from \\\"@philiprehberger/growth\\\";\\n\\nconst growth = new Growth({\\n pennant: new Pennant({\\n apiKey: process.env.PENNANT_KEY!,\\n context: { userId: \\\"alice\\\" },\\n }),\\n splitstream: new Splitstream({\\n apiKey: process.env.SPLITSTREAM_KEY!,\\n context: { userId: \\\"alice\\\" },\\n }),\\n});\\n\\nconst { variant, killSwitchActive, track } = await growth.useExperiment(\\n \\\"checkout-v2\\\",\\n { fallback: \\\"control\\\" }\\n);\\n\\nif (killSwitchActive) {\\n // Pennant flag was off — Splitstream was NOT consulted.\\n // Exposure was NOT logged. Returns 'control' (or your controlKey).\\n} else {\\n // Pennant flag was on — Splitstream.assign() ran, exposure logged.\\n // variant ∈ {'control', 'treatment', null}.\\n}\\n\\ntrack(\\\"purchase-completed\\\", { revenue: 49 });\"}]}],\"$L7\",\"$L8\",\"$L9\",\"$La\",\"$Lb\",\"$Lc\",\"$Ld\",\"$Le\"]}],null,\"$Lf\"]}],{},null,false,null]},null,false,\"$@10\"]},null,false,null],\"$L11\",false]],\"m\":\"$undefined\",\"G\":[\"$12\",[\"$L13\"]],\"S\":true,\"h\":null,\"s\":\"$undefined\",\"l\":\"$undefined\",\"p\":\"$undefined\",\"d\":\"$undefined\",\"b\":\"d4p1Nsp7dnkNrIm-L0oJl\"}\n"])</script><script>self.__next_f.push([1,"14:I[51858,[\"/_next/static/chunks/3u-4v10_yuvs8.js\",\"/_next/static/chunks/31jk64s9j6agx.js\"],\"OutletBoundary\"]\n15:\"$Sreact.suspense\"\n18:I[51858,[\"/_next/static/chunks/3u-4v10_yuvs8.js\",\"/_next/static/chunks/31jk64s9j6agx.js\"],\"ViewportBoundary\"]\n1a:I[51858,[\"/_next/static/chunks/3u-4v10_yuvs8.js\",\"/_next/static/chunks/31jk64s9j6agx.js\"],\"MetadataBoundary\"]\n7:[\"$\",\"h2\",null,{\"children\":\"What the umbrella does (and doesn't)\"}]\n8:[\"$\",\"ul\",null,{\"children\":[[\"$\",\"li\",null,{\"children\":[[\"$\",\"strong\",null,{\"children\":\"Does:\"}],\" read the Pennant flag with the same key shape (\",[\"$\",\"code\",null,{\"children\":\"$${experimentKey}-enabled\"}],\" by default), gate the Splitstream call accordingly, expose a unified\",\" \",[\"$\",\"code\",null,{\"children\":\"track()\"}],\" callback. Total wrapper LOC: ~150.\"]}],[\"$\",\"li\",null,{\"children\":[[\"$\",\"strong\",null,{\"children\":\"Doesn't:\"}],\"own state, persist anything, or replace either client's features. Both work standalone.\"]}]]}]\n9:[\"$\",\"h2\",null,{\"children\":\"Override the kill-switch convention\"}]\na:[\"$\",\"pre\",null,{\"children\":[\"$\",\"code\",null,{\"children\":\"new Growth({\\n pennant,\\n splitstream,\\n killSwitchKeyFor: (experimentKey) =\u003e `exp.${experimentKey}.gate`,\\n killSwitchDefault: false, // fail closed when the flag isn't in the snapshot\\n});\"}]}]\nb:[\"$\",\"h2\",null,{\"children\":\"React\"}]\nc:[\"$\",\"pre\",null,{\"children\":[\"$\",\"code\",null,{\"children\":\"import { GrowthProvider, useExperiment } from \\\"@philiprehberger/growth/react\\\";\\n\\nfunction App() {\\n return (\\n \u003cGrowthProvider growth={growth}\u003e\\n \u003cCheckoutButton /\u003e\\n \u003c/GrowthProvider\u003e\\n );\\n}\\n\\nfunction CheckoutButton() {\\n const { variant, killSwitchActive, isLoading, track } = useExperiment(\\\"checkout-v2\\\", {\\n fallback: \\\"control\\\",\\n });\\n if (isLoading) return \u003cbutton disabled\u003eLoading…\u003c/button\u003e;\\n return (\\n \u003cbutton\\n className={variant === \\\"treatment\\\" ? \\\"bg-green-500\\\" : \\\"bg-blue-500\\\"}\\n onClick={() =\u003e track(\\\"purchase-completed\\\", { revenue: 49 })}\\n \u003e\\n Buy now\\n \u003c/button\u003e\\n );\\n}\"}]}]\nd:[\"$\",\"h2\",null,{\"children\":\"Why ship them as separate products\"}]\ne:[\"$\",\"p\",null,{\"children\":\"Same buyer overlap, different artifacts. Pennant's headliner is the SDK + SSE story. Splitstream's headliner is the assignment API + the credible-interval readout. Sharing a repo would muddle both pitches. They cross-link from the docs sites, and the joint sample is the same five lines on both. The umbrella exists so the joint pitch can be one call.\"}]\nf:[\"$\",\"$L14\",null,{\"children\":[\"$\",\"$15\",null,{\"name\":\"Next.MetadataOutlet\",\"children\":\"$@16\"}]}]\n17:[]\n10:\"$W17\"\n11:[\"$\",\"$1\",\"h\",{\"children\":[null,[\"$\",\"$L18\",null,{\"children\":\"$L19\"}],[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$L1a\",null,{\"children\":[\"$\",\"$15\",null,{\"name\":\"Next.Metadata\",\"children\":\"$L1b\"}]}]}],null]}]\n13:[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/002zte3riz0h6.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]\n"])</script><script>self.__next_f.push([1,"19:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n"])</script><script>self.__next_f.push([1,"16:null\n1b:[[\"$\",\"title\",\"0\",{\"children\":\"With Pennant — Splitstream\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Production-shaped A/B testing platform: sticky bucketing, mutex experiment groups, Sample Ratio Mismatch detection, guardrail metrics, an Octane carve-out for the hot path, and a Bayesian stats engine whose decision rule is empirically calibrated — not asserted.\"}]]\n"])</script></body></html>