diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index fb565b8..789a289 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -15,20 +15,22 @@ permissions: security-events: write jobs: - govulncheck: - name: govulncheck - runs-on: ubuntu-latest - strategy: - fail-fast: false - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 + # govulncheck: + # name: govulncheck + # runs-on: ubuntu-latest + # strategy: + # fail-fast: false + # steps: + # - uses: actions/checkout@v6 + # - uses: actions/setup-go@v6 + # with: + # go-version-file: go.mod - - name: Install govulncheck - run: go install golang.org/x/vuln/cmd/govulncheck@latest + # - name: Install govulncheck + # run: go install golang.org/x/vuln/cmd/govulncheck@latest - - name: govulncheck - run: govulncheck ./... + # - name: govulncheck + # run: govulncheck ./... bearer: name: bearer @@ -50,6 +52,8 @@ jobs: steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 + with: + go-version-file: go.mod - name: Initialize CodeQL uses: github/codeql-action/init@v4 @@ -69,6 +73,8 @@ jobs: steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 + with: + go-version-file: go.mod - name: Run Trivy vulnerability scanner (source code) uses: aquasecurity/trivy-action@0.35.0 @@ -80,6 +86,7 @@ jobs: output: "trivy-results.sarif" severity: "CRITICAL,HIGH,MEDIUM" ignore-unfixed: true + trivyignores: ".trivyignore" skip-dirs: "docs/" - name: Upload Trivy results to GitHub Security tab @@ -96,6 +103,8 @@ jobs: steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 + with: + go-version-file: go.mod - name: Run Trivy scanner (table output for logs) uses: aquasecurity/trivy-action@0.35.0 @@ -107,6 +116,7 @@ jobs: format: "table" severity: "CRITICAL,HIGH,MEDIUM" ignore-unfixed: true + trivyignores: ".trivyignore" exit-code: "1" skip-dirs: "docs/" diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index bfd68d2..45a12e2 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -59,10 +59,11 @@ const config: Config = { // Optional: Enable hash router for offline support (experimental) // Uncomment if you need offline browsing capability // router: 'hash', - - // Future-proofing configurations + + // Future-proofing configurations clientModules: [ require.resolve('./src/theme/prism-include-languages.js'), + require.resolve('./src/clientModules/posthog-events.ts'), ], // Even if you don't use internationalization, you can use this field to set @@ -170,8 +171,8 @@ const config: Config = { // Enhanced markdown features remarkPlugins: [], rehypePlugins: [], - }, - sitemap: { + }, + sitemap: { lastmod: 'date', changefreq: 'weekly', priority: 0.7, @@ -220,8 +221,8 @@ const config: Config = { maxTextSize: 50000, }, }, - - // Enhanced metadata + + // Enhanced metadata metadata: [ {name: 'og:type', content: 'website'}, ], @@ -238,8 +239,8 @@ const config: Config = { sidebarId: 'docSidebar', position: 'left', label: 'Doc', - }, - { + }, + { to: 'https://pkg.go.dev/github.com/samber/lo', label: 'GoDoc', position: 'left', @@ -363,10 +364,19 @@ const config: Config = { } satisfies Preset.ThemeConfig, themes: ['@docusaurus/theme-mermaid'], - - plugins: [ - // Add ideal image plugin for better image optimization - [ + + plugins: [ + [ + "posthog-docusaurus", + { + apiKey: "phc_uA762TtYyJ6UrbF5nzWutAJojstpC2EDptFpd2bBvWFY", + appHost: "https://hogpost.samber.dev", + enableInDevelopment: false, // optional, + disableSessionRecording: true, + }, + ], + // Add ideal image plugin for better image optimization + [ '@docusaurus/plugin-ideal-image', { quality: 70, diff --git a/docs/package-lock.json b/docs/package-lock.json index 3652e08..0d8f7df 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -21,6 +21,7 @@ "classnames": "^2.3.2", "clsx": "^2.0.0", "marked": "^17.0.1", + "posthog-docusaurus": "^2.0.5", "prism-react-renderer": "^2.3.0", "prismjs": "^1.29.0", "react": "^19.0.0", @@ -17279,6 +17280,15 @@ "postcss": "^8.4.31" } }, + "node_modules/posthog-docusaurus": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/posthog-docusaurus/-/posthog-docusaurus-2.0.5.tgz", + "integrity": "sha512-Ray65LYEJrMMqDtsBUBXunEVP/g4wtATvq/xz9rchUoLy/9mSkkFgUko/8DVtGxgiP3vivpFMgfb9HpCuDrBHg==", + "license": "MIT", + "engines": { + "node": ">=10.15.1" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", diff --git a/docs/package.json b/docs/package.json index 280e830..033f26d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -35,6 +35,7 @@ "classnames": "^2.3.2", "clsx": "^2.0.0", "marked": "^17.0.1", + "posthog-docusaurus": "^2.0.5", "prism-react-renderer": "^2.3.0", "prismjs": "^1.29.0", "react": "^19.0.0", diff --git a/docs/plugins/helpers-pages/components/HelperCard.tsx b/docs/plugins/helpers-pages/components/HelperCard.tsx index 9d5a565..39d5d3d 100644 --- a/docs/plugins/helpers-pages/components/HelperCard.tsx +++ b/docs/plugins/helpers-pages/components/HelperCard.tsx @@ -6,6 +6,14 @@ import Heading from '@theme/Heading'; import CodeBlock from '@theme/CodeBlock'; import '../../../src/prism-include-languages.js'; +declare global { + interface Window { + posthog?: { + capture: (event: string, properties?: Record) => void; + }; + } +} + interface HelperCardProps { helper: HelperDefinition; } @@ -125,31 +133,34 @@ export default function HelperCard({ {sourceRef && ( - window.posthog?.capture('helper_source_clicked', { helper: helper.slug, category: helper.category })} > 🧩 Source )} {godocUrl && ( - window.posthog?.capture('helper_godoc_clicked', { helper: helper.slug, category: helper.category })} > 📚 GoDoc )} {helper.playUrl && ( - window.posthog?.capture('helper_playground_clicked', { helper: helper.slug, category: helper.category })} > 🎮 Try on Go Playground @@ -243,10 +254,11 @@ function SimilarHelpers({ const displayName = nameRaw || name; const isSameSection = type === currentTypeLower; // compare only type for label return ( - window.posthog?.capture('helper_similar_clicked', { from: currentName, to: name, title: title.toLowerCase() })} > {isSameSection ? ( displayName diff --git a/docs/src/clientModules/posthog-events.ts b/docs/src/clientModules/posthog-events.ts new file mode 100644 index 0000000..0e2df41 --- /dev/null +++ b/docs/src/clientModules/posthog-events.ts @@ -0,0 +1,53 @@ +declare global { + interface Window { + posthog?: { + capture: (event: string, properties?: Record) => void; + }; + } +} + +// --- Sponsor click tracking (navbar + sidebar) --- +function trackSponsorClicks(): void { + document.addEventListener('click', (e) => { + const anchor = (e.target as Element).closest('a[href*="sponsors/samber"]'); + if (!anchor) return; + + const isNavbar = anchor.closest('.navbar') !== null; + window.posthog?.capture('sponsor_clicked', { + location: isNavbar ? 'navbar' : 'sidebar', + href: (anchor as HTMLAnchorElement).href, + }); + }); +} + +// --- Search query tracking --- +function trackSearch(): void { + let inputEl: HTMLInputElement | null = null; + + const attachInputListener = (input: HTMLInputElement) => { + if (input === inputEl) return; + inputEl = input; + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && input.value.trim()) { + window.posthog?.capture('search_submitted', { + query: input.value.trim(), + }); + } + }); + }; + + const observer = new MutationObserver(() => { + const input = document.querySelector('.DocSearch-Input'); + if (input) attachInputListener(input); + }); + + observer.observe(document.body, {childList: true, subtree: true}); +} + +export function onRouteDidUpdate(): void { + // Re-run on each navigation in case DOM changed +} + +// Runs once on initial load +trackSponsorClicks(); +trackSearch(); diff --git a/docs/src/theme/CodeBlock/Buttons/CopyButton/index.tsx b/docs/src/theme/CodeBlock/Buttons/CopyButton/index.tsx new file mode 100644 index 0000000..e99e1f3 --- /dev/null +++ b/docs/src/theme/CodeBlock/Buttons/CopyButton/index.tsx @@ -0,0 +1,30 @@ +import React, {useCallback, type ReactNode} from 'react'; +import CopyButton from '@theme-original/CodeBlock/Buttons/CopyButton'; +import {useCodeBlockContext} from '@docusaurus/theme-common/internal'; +import type {Props} from '@theme/CodeBlock/Buttons/CopyButton'; + +declare global { + interface Window { + posthog?: { + capture: (event: string, properties?: Record) => void; + }; + } +} + +export default function CopyButtonWrapper(props: Props): ReactNode { + const {metadata} = useCodeBlockContext(); + + const handleClick = useCallback(() => { + window.posthog?.capture('code_copied', { + helper: window.location.hash.replace('#', '') || null, + page: window.location.pathname, + code_preview: metadata.code?.slice(0, 120), + }); + }, [metadata.code]); + + return ( + + + + ); +} diff --git a/docs/src/theme/ColorModeToggle/index.tsx b/docs/src/theme/ColorModeToggle/index.tsx new file mode 100644 index 0000000..e6ba014 --- /dev/null +++ b/docs/src/theme/ColorModeToggle/index.tsx @@ -0,0 +1,18 @@ +import React, {type ReactNode} from 'react'; +import OriginalColorModeToggle from '@theme-original/ColorModeToggle'; +import type ColorModeToggleType from '@theme/ColorModeToggle'; +import type {WrapperProps} from '@docusaurus/types'; + +type Props = WrapperProps; + +export default function ColorModeToggleWrapper(props: Props): ReactNode { + const handleChange: Props['onChange'] = (newMode) => { + window.posthog?.capture('color_mode_toggled', { + from: props.value ?? 'system', + to: newMode ?? 'system', + }); + props.onChange(newMode); + }; + + return ; +}