diff --git a/.github/workflows/vercel-preview.yaml b/.github/workflows/vercel-preview.yaml index 221656e09e..7645451713 100644 --- a/.github/workflows/vercel-preview.yaml +++ b/.github/workflows/vercel-preview.yaml @@ -17,5 +17,7 @@ jobs: run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} - name: Build Project Artifacts run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + env: + NEXT_PUBLIC_SPRIG_ENVIRONMENT_ID: ${{ secrets.SPRIG_ENVIRONMENT_ID }} - name: Deploy Project Artifacts to Vercel run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} diff --git a/SPRIG_SETUP.md b/SPRIG_SETUP.md new file mode 100644 index 0000000000..7896f713f4 --- /dev/null +++ b/SPRIG_SETUP.md @@ -0,0 +1,48 @@ +# Sprig Survey Setup + +## Overview +Sprig surveys are configured to trigger on the experimentation docs page to gather user feedback. + +## Setup Instructions + +### 1. Environment Variables +To enable Sprig surveys, you need to set the following environment variable: + +``` +NEXT_PUBLIC_SPRIG_ENVIRONMENT_ID=your-sprig-environment-id +``` + +### 2. Local Development +For local development, create a `.env.local` file in the root directory: + +```bash +# .env.local +NEXT_PUBLIC_SPRIG_ENVIRONMENT_ID=your-sprig-environment-id +``` + +### 3. Production Deployment +For production, set the environment variable in your deployment platform: + +- **Vercel**: Add the environment variable in your project settings +- **GitHub Actions**: Add as a repository secret and reference in your deployment workflow +- **Other platforms**: Follow your platform's documentation for setting environment variables + +### 4. Survey Configuration +The survey is triggered when users visit the `/docs/experiments` page. The tracking event name is `viewed_experimentation_docs`. + +In your Sprig dashboard: +1. Create a survey +2. Set up targeting based on the `viewed_experimentation_docs` event +3. Configure your survey questions and appearance + +### 5. Security Notes +- The environment variable is prefixed with `NEXT_PUBLIC_` because it needs to be available in the browser +- The Sprig Environment ID is not sensitive like an API key, but should still be managed securely +- If the environment variable is not set, Sprig will not initialize and no surveys will be shown + +## Testing +To test the integration: +1. Set up your local environment with the Sprig Environment ID +2. Navigate to `/docs/experiments` +3. Check your Sprig dashboard for the tracking event +4. Verify that surveys are displayed according to your targeting rules \ No newline at end of file diff --git a/hooks/useSprig.ts b/hooks/useSprig.ts new file mode 100644 index 0000000000..1f1a69101a --- /dev/null +++ b/hooks/useSprig.ts @@ -0,0 +1,99 @@ +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; + +// Types for Sprig global interface +declare global { + interface Window { + Sprig: { + (method: string, ...args: any[]): void; + appId?: string; + _API_URL?: string; + _queue?: any[]; + }; + } +} + +// Track last event time to prevent duplicates +let lastEventTime = 0; +const EVENT_DEBOUNCE_MS = 1000; // 1 second + +export const useSprig = () => { + const router = useRouter(); + const sprigEnvironmentId = process.env.NEXT_PUBLIC_SPRIG_ENVIRONMENT_ID; + + // Early return for server-side rendering + if (typeof window === 'undefined') { + return; + } + + // Initialize Sprig script + useEffect(() => { + if (!sprigEnvironmentId || typeof window === 'undefined') return; + + // Check if Sprig is already initialized + if (window.Sprig) return; + + try { + // Official Sprig initialization pattern + window.Sprig = function(method: string, ...args: any[]) { + (window.Sprig._queue = window.Sprig._queue || []).push([method, ...args]); + }; + + // Configure with environment ID + window.Sprig.appId = sprigEnvironmentId; + window.Sprig._API_URL = 'https://api.sprig.com'; + + // Load the Sprig script + const script = document.createElement('script'); + script.async = true; + script.src = 'https://cdn.sprig.com/shim.js'; + + script.onerror = () => { + console.error('Failed to load Sprig script'); + }; + + document.head.appendChild(script); + } catch (error) { + console.error('Sprig initialization failed:', error); + } + }, [sprigEnvironmentId]); + + // Track experimentation page visits + useEffect(() => { + if (!sprigEnvironmentId || typeof window === 'undefined') return; + + const trackExperimentView = () => { + const now = Date.now(); + + // Prevent duplicate events within debounce period + if (now - lastEventTime < EVENT_DEBOUNCE_MS) { + return; + } + + if (!window.Sprig) { + return; + } + + try { + lastEventTime = now; + window.Sprig('track', 'viewed_experimentation_docs'); + } catch (error) { + console.error('Sprig track failed:', error); + } + }; + + const handleRouteChange = (url: string) => { + if (url.includes('/docs/experiments')) { + trackExperimentView(); + } + }; + + // Track if already on experimentation page + if (router.asPath.includes('/docs/experiments')) { + trackExperimentView(); + } + + router.events.on('routeChangeComplete', handleRouteChange); + return () => router.events.off('routeChangeComplete', handleRouteChange); + }, [router, sprigEnvironmentId]); +}; \ No newline at end of file diff --git a/middleware.ts b/middleware.ts index 567c34a867..9b5df4907d 100644 --- a/middleware.ts +++ b/middleware.ts @@ -6,7 +6,7 @@ export function middleware() { // Set custom header response.headers.set(`X-Frame-Options`, `deny`); response.headers.set(`X-Content-Type-Options`, `nosniff`); - response.headers.set(`Content-Security-Policy`, `default-src 'self' https://*.mixpanel.com https://cdn.mxpnl.com https://cdn-dev.mxpnl.com https://ekr.zendesk.com wss://mixpanelsupport.zendesk.com https://mixpanel.com https://*.mixpanel.com; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://*.kapa.ai https://*.hotjar.com https://cdn.mxpnl.com https://cdn-dev.mxpnl.com https://cdn.rollbar.com https://*.zopim.com https://assets.zendesk.com https://www.youtube.com/embed/ https://connect.facebook.net https://apis.google.com https://accounts.google.com 'unsafe-eval' https://*.6sc.co https://*.bing.com https://*.clarity.ms https://cdnjs.cloudflare.com d2wy8f7a9ursnm.cloudfront.net https://*.doubleclick.net https://*.google.com https://*.google-analytics.com https://*.googlesyndication.com https://www.googletagmanager.com https://*.gstatic.cn https://*.gstatic.com https://*.g2crowd.com https://snap.licdn.com https://*.marketo.com https://*.marketo.net https://mixpanel.com https://*.mixpanel.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://recaptcha.net https://*.recaptcha.net https://www.redditstatic.com/ads/ https://*.singular.net https://*.ads-twitter.com https://mxpnlcms.wpengine.com https://*.youtube.com https://*.zoominfo.com; connect-src 'self' blob: data: https://kapa-widget-proxy-la7dkmplpq-uc.a.run.app https://*.hotjar.com wss://*.hotjar.com https://*.hotjar.io https://*.mixpanel.com https://cdn.mxpnl.com https://cdn-dev.mxpnl.com https://api.rollbar.com https://api.sprig.com https://*.zdassets.com https://mixpanelsupport.zendesk.com https://*.zopim.com wss://*.zopim.com https://storage.googleapis.com https://*.facebook.com https://*.6sc.co https://*.adnxs.com https://*.bing.com https://*.bugsnag.com https://*.clarity.ms https://*.doubleclick.net https://*.google.com https://*.google-analytics.com https://*.googlesyndication.com https://*.g2crowd.com https://*.linkedin.com https://mixpanel.com https://*.mixpanel.com https://*.mktoresp.com https://cdn.linkedin.oribi.io https://*.reddit.com https://www.redditstatic.com/ads/ https://*.singular.net https://mxpnlcms.wpengine.com https://*.zoominfo.com; media-src 'self' https://user-images.githubusercontent.com; img-src 'self' blob: data: https://www.google.com https://*.gstatic.com https://t2.gstatic.com https://github-production-user-asset-6210df.s3.amazonaws.com https://github.com https://user-images.githubusercontent.com https://avatars.githubusercontent.com/ https://*.chmln-cdn.com https://cdn.mxpnl.com https://cdn-dev.mxpnl.com https://*.zdassets.com https://*.zopim.com https://v2uploads.zopim.io https://*.facebook.com https://*.gravatar.com https://*.wp.com https://*.6sc.co https://*.adnxs.com https://*.adroll.com https://*.bidswitch.net https://*.bing.com https://cdn.bizible.com https://cdn.bizibly.com https://*.bugsnag.com https://*.casalemedia.com https://*.clarity.ms https://*.crwdcntrl.net https://*.doubleclick.net https://*.exelator.com https://*.google-analytics.com https://*.googlesyndication.com https://www.googletagmanager.com https://*.googleusercontent.com https://*.imrworldwide.com https://*.linkedin.com https://mixpanel.com https://*.mixpanel.com https://*.reddit.com https://t.co/ https://analytics.twitter.com https://*.wpengine.com https://mxpnlcms.wpengine.com https://mxpnlcms.wpenginepowered.com https://*.analytics.yahoo.com https://*.google.com https://*.google.ad https://*.google.ae https://*.google.com.af https://*.google.com.ag https://*.google.al https://*.google.am https://*.google.co.ao https://*.google.com.ar https://*.google.as https://*.google.at https://*.google.com.au https://*.google.az https://*.google.ba https://*.google.com.bd https://*.google.be https://*.google.bf https://*.google.bg https://*.google.com.bh https://*.google.bi https://*.google.bj https://*.google.com.bn https://*.google.com.bo https://*.google.com.br https://*.google.bs https://*.google.bt https://*.google.co.bw https://*.google.by https://*.google.com.bz https://*.google.ca https://*.google.cd https://*.google.cf https://*.google.cg https://*.google.ch https://*.google.ci https://*.google.co.ck https://*.google.cl https://*.google.cm https://*.google.cn https://*.google.com.co https://*.google.co.cr https://*.google.com.cu https://*.google.cv https://*.google.com.cy https://*.google.cz https://*.google.de https://*.google.dj https://*.google.dk https://*.google.dm https://*.google.com.do https://*.google.dz https://*.google.com.ec https://*.google.ee https://*.google.com.eg https://*.google.es https://*.google.com.et https://*.google.fi https://*.google.com.fj https://*.google.fm https://*.google.fr https://*.google.ga https://*.google.ge https://*.google.gg https://*.google.com.gh https://*.google.com.gi https://*.google.gl https://*.google.gm https://*.google.gr https://*.google.com.gt https://*.google.gy https://*.google.com.hk https://*.google.hn https://*.google.hr https://*.google.ht https://*.google.hu https://*.google.co.id https://*.google.ie https://*.google.co.il https://*.google.im https://*.google.co.in https://*.google.iq https://*.google.is https://*.google.it https://*.google.je https://*.google.com.jm https://*.google.jo https://*.google.co.jp https://*.google.co.ke https://*.google.com.kh https://*.google.ki https://*.google.kg https://*.google.co.kr https://*.google.com.kw https://*.google.kz https://*.google.la https://*.google.com.lb https://*.google.li https://*.google.lk https://*.google.co.ls https://*.google.lt https://*.google.lu https://*.google.lv https://*.google.com.ly https://*.google.co.ma https://*.google.md https://*.google.me https://*.google.mg https://*.google.mk https://*.google.ml https://*.google.com.mm https://*.google.mn https://*.google.com.mt https://*.google.mu https://*.google.mv https://*.google.mw https://*.google.com.mx https://*.google.com.my https://*.google.co.mz https://*.google.com.na https://*.google.com.ng https://*.google.com.ni https://*.google.ne https://*.google.nl https://*.google.no https://*.google.com.np https://*.google.nr https://*.google.nu https://*.google.co.nz https://*.google.com.om https://*.google.com.pa https://*.google.com.pe https://*.google.com.pg https://*.google.com.ph https://*.google.com.pk https://*.google.pl https://*.google.pn https://*.google.com.pr https://*.google.ps https://*.google.pt https://*.google.com.py https://*.google.com.qa https://*.google.ro https://*.google.ru https://*.google.rw https://*.google.com.sa https://*.google.com.sb https://*.google.sc https://*.google.se https://*.google.com.sg https://*.google.sh https://*.google.si https://*.google.sk https://*.google.com.sl https://*.google.sn https://*.google.so https://*.google.sm https://*.google.sr https://*.google.st https://*.google.com.sv https://*.google.td https://*.google.tg https://*.google.co.th https://*.google.com.tj https://*.google.tl https://*.google.tm https://*.google.tn https://*.google.to https://*.google.com.tr https://*.google.tt https://*.google.com.tw https://*.google.co.tz https://*.google.com.ua https://*.google.co.ug https://*.google.co.uk https://*.google.com.uy https://*.google.co.uz https://*.google.com.vc https://*.google.co.ve https://*.google.co.vi https://*.google.com.vn https://*.google.vu https://*.google.ws https://*.google.rs https://*.google.co.za https://*.google.co.zm https://*.google.co.zw https://*.google.cat; style-src 'self' 'unsafe-inline' https://cdn.mxpnl.com https://cdn-dev.mxpnl.com https://*.google.com https://*.marketo.com https://mixpanel.com https://*.mixpanel.com https://hello.myfonts.net; font-src 'self' data: https://cdn.mxpnl.com https://cdn-dev.mxpnl.com https://mixpanel.com https://*.mixpanel.com; frame-src 'self' https://www.googletagmanager.com https://js.stripe.com https://www.loom.com/embed/ https://player.vimeo.com/video/ https://www.youtube.com/embed/ https://*.facebook.com https://accounts.google.com https://static.addtoany.com https://*.bing.com https://*.doubleclick.net https://*.google.com https://*.marketo.com https://mixpanel.com https://*.mixpanel.com https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/ https://recaptcha.net https://*.recaptcha.net; worker-src 'self' blob:;`); + response.headers.set(`Content-Security-Policy`, `default-src 'self' https://*.mixpanel.com https://cdn.mxpnl.com https://cdn-dev.mxpnl.com https://ekr.zendesk.com wss://mixpanelsupport.zendesk.com https://mixpanel.com https://*.mixpanel.com; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://*.kapa.ai https://*.hotjar.com https://cdn.mxpnl.com https://cdn-dev.mxpnl.com https://cdn.rollbar.com https://*.zopim.com https://assets.zendesk.com https://www.youtube.com/embed/ https://connect.facebook.net https://apis.google.com https://accounts.google.com 'unsafe-eval' https://*.6sc.co https://*.bing.com https://*.clarity.ms https://cdnjs.cloudflare.com d2wy8f7a9ursnm.cloudfront.net https://*.doubleclick.net https://*.google.com https://*.google-analytics.com https://*.googlesyndication.com https://www.googletagmanager.com https://*.gstatic.cn https://*.gstatic.com https://*.g2crowd.com https://snap.licdn.com https://*.marketo.com https://*.marketo.net https://mixpanel.com https://*.mixpanel.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://recaptcha.net https://*.recaptcha.net https://www.redditstatic.com/ads/ https://*.singular.net https://*.ads-twitter.com https://mxpnlcms.wpengine.com https://*.youtube.com https://*.zoominfo.com https://cdn.sprig.com; connect-src 'self' blob: data: https://kapa-widget-proxy-la7dkmplpq-uc.a.run.app https://*.hotjar.com wss://*.hotjar.com https://*.hotjar.io https://*.mixpanel.com https://cdn.mxpnl.com https://cdn-dev.mxpnl.com https://api.rollbar.com https://api.sprig.com https://cdn.sprig.com https://*.zdassets.com https://mixpanelsupport.zendesk.com https://*.zopim.com wss://*.zopim.com https://storage.googleapis.com https://*.facebook.com https://*.6sc.co https://*.adnxs.com https://*.bing.com https://*.bugsnag.com https://*.clarity.ms https://*.doubleclick.net https://*.google.com https://*.google-analytics.com https://*.googlesyndication.com https://*.g2crowd.com https://*.linkedin.com https://mixpanel.com https://*.mixpanel.com https://*.mktoresp.com https://cdn.linkedin.oribi.io https://*.reddit.com https://www.redditstatic.com/ads/ https://*.singular.net https://mxpnlcms.wpengine.com https://*.zoominfo.com; media-src 'self' https://user-images.githubusercontent.com; img-src 'self' blob: data: https://www.google.com https://*.gstatic.com https://t2.gstatic.com https://github-production-user-asset-6210df.s3.amazonaws.com https://github.com https://user-images.githubusercontent.com https://avatars.githubusercontent.com/ https://*.chmln-cdn.com https://cdn.mxpnl.com https://cdn-dev.mxpnl.com https://*.zdassets.com https://*.zopim.com https://v2uploads.zopim.io https://*.facebook.com https://*.gravatar.com https://*.wp.com https://*.6sc.co https://*.adnxs.com https://*.adroll.com https://*.bidswitch.net https://*.bing.com https://cdn.bizible.com https://cdn.bizibly.com https://*.bugsnag.com https://*.casalemedia.com https://*.clarity.ms https://*.crwdcntrl.net https://*.doubleclick.net https://*.exelator.com https://*.google-analytics.com https://*.googlesyndication.com https://www.googletagmanager.com https://*.googleusercontent.com https://*.imrworldwide.com https://*.linkedin.com https://mixpanel.com https://*.mixpanel.com https://*.reddit.com https://t.co/ https://analytics.twitter.com https://*.wpengine.com https://mxpnlcms.wpengine.com https://mxpnlcms.wpenginepowered.com https://*.analytics.yahoo.com https://*.google.com https://*.google.ad https://*.google.ae https://*.google.com.af https://*.google.com.ag https://*.google.al https://*.google.am https://*.google.co.ao https://*.google.com.ar https://*.google.as https://*.google.at https://*.google.com.au https://*.google.az https://*.google.ba https://*.google.com.bd https://*.google.be https://*.google.bf https://*.google.bg https://*.google.com.bh https://*.google.bi https://*.google.bj https://*.google.com.bn https://*.google.com.bo https://*.google.com.br https://*.google.bs https://*.google.bt https://*.google.co.bw https://*.google.by https://*.google.com.bz https://*.google.ca https://*.google.cd https://*.google.cf https://*.google.cg https://*.google.ch https://*.google.ci https://*.google.co.ck https://*.google.cl https://*.google.cm https://*.google.cn https://*.google.com.co https://*.google.co.cr https://*.google.com.cu https://*.google.cv https://*.google.com.cy https://*.google.cz https://*.google.de https://*.google.dj https://*.google.dk https://*.google.dm https://*.google.com.do https://*.google.dz https://*.google.com.ec https://*.google.ee https://*.google.com.eg https://*.google.es https://*.google.com.et https://*.google.fi https://*.google.com.fj https://*.google.fm https://*.google.fr https://*.google.ga https://*.google.ge https://*.google.gg https://*.google.com.gh https://*.google.com.gi https://*.google.gl https://*.google.gm https://*.google.gr https://*.google.com.gt https://*.google.gy https://*.google.com.hk https://*.google.hn https://*.google.hr https://*.google.ht https://*.google.hu https://*.google.co.id https://*.google.ie https://*.google.co.il https://*.google.im https://*.google.co.in https://*.google.iq https://*.google.is https://*.google.it https://*.google.je https://*.google.com.jm https://*.google.jo https://*.google.co.jp https://*.google.co.ke https://*.google.com.kh https://*.google.ki https://*.google.kg https://*.google.co.kr https://*.google.com.kw https://*.google.kz https://*.google.la https://*.google.com.lb https://*.google.li https://*.google.lk https://*.google.co.ls https://*.google.lt https://*.google.lu https://*.google.lv https://*.google.com.ly https://*.google.co.ma https://*.google.md https://*.google.me https://*.google.mg https://*.google.mk https://*.google.ml https://*.google.com.mm https://*.google.mn https://*.google.com.mt https://*.google.mu https://*.google.mv https://*.google.mw https://*.google.com.mx https://*.google.com.my https://*.google.co.mz https://*.google.com.na https://*.google.com.ng https://*.google.com.ni https://*.google.ne https://*.google.nl https://*.google.no https://*.google.com.np https://*.google.nr https://*.google.nu https://*.google.co.nz https://*.google.com.om https://*.google.com.pa https://*.google.com.pe https://*.google.com.pg https://*.google.com.ph https://*.google.com.pk https://*.google.pl https://*.google.pn https://*.google.com.pr https://*.google.ps https://*.google.pt https://*.google.com.py https://*.google.com.qa https://*.google.ro https://*.google.ru https://*.google.rw https://*.google.com.sa https://*.google.com.sb https://*.google.sc https://*.google.se https://*.google.com.sg https://*.google.sh https://*.google.si https://*.google.sk https://*.google.com.sl https://*.google.sn https://*.google.so https://*.google.sm https://*.google.sr https://*.google.st https://*.google.com.sv https://*.google.td https://*.google.tg https://*.google.co.th https://*.google.com.tj https://*.google.tl https://*.google.tm https://*.google.tn https://*.google.to https://*.google.com.tr https://*.google.tt https://*.google.com.tw https://*.google.co.tz https://*.google.com.ua https://*.google.co.ug https://*.google.co.uk https://*.google.com.uy https://*.google.co.uz https://*.google.com.vc https://*.google.co.ve https://*.google.co.vi https://*.google.com.vn https://*.google.vu https://*.google.ws https://*.google.rs https://*.google.co.za https://*.google.co.zm https://*.google.co.zw https://*.google.cat; style-src 'self' 'unsafe-inline' https://cdn.mxpnl.com https://cdn-dev.mxpnl.com https://*.google.com https://*.marketo.com https://mixpanel.com https://*.mixpanel.com https://hello.myfonts.net; font-src 'self' data: https://cdn.mxpnl.com https://cdn-dev.mxpnl.com https://mixpanel.com https://*.mixpanel.com; frame-src 'self' https://www.googletagmanager.com https://js.stripe.com https://www.loom.com/embed/ https://player.vimeo.com/video/ https://www.youtube.com/embed/ https://*.facebook.com https://accounts.google.com https://static.addtoany.com https://*.bing.com https://*.doubleclick.net https://*.google.com https://*.marketo.com https://mixpanel.com https://*.mixpanel.com https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/ https://recaptcha.net https://*.recaptcha.net; worker-src 'self' blob:;`); response.headers.set(`X-Permitted-Cross-Domain-Policies`, `none`); response.headers.set(`Referrer-Policy`, `strict-origin`); response.headers.set(`Permissions-Policy`, `accelerometer=(), camera=(), encrypted-media=(self "https://www.youtube.com" "https://www.loom.com"), fullscreen=(self "https://www.youtube.com" "https://www.loom.com"), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), usb=()`); diff --git a/pages/_app.tsx b/pages/_app.tsx index 010672ccf1..dd87314e7a 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -7,11 +7,16 @@ import * as Sentry from "@sentry/react"; import { insertGTMScriptTags } from "../components/GTMScripts"; import { SENTRY_VARS } from "../utils/error-reporting"; +import { useSprig } from "../hooks/useSprig"; export default function MyApp({ Component, pageProps }: AppProps) { + // Initialize Sprig surveys + useSprig(); + useEffect(() => { insertGTMScriptTags(); window.sentry = Sentry.init(SENTRY_VARS); + // TODO: Based on their doc: https://docs.kapa.ai/integrations/website-widget/javascript-api/events // we should be able to use `Kapa` as a function like below, but it seems like the global object interface has changed // which is throwing Kapa is not a function error. I will follow up w/ Kapa team to see what's up.