Add PostHog consent integration and extract integration plugin system
Add PostHog opt-in/opt-out consent support mirroring the existing Google Consent Mode integration: database column, GraphQL field, console UI toggle, and client-side posthog-js calls. Extract both GCM and PostHog logic from CookieBannerClient into a ConsentIntegration plugin interface so future integrations can be added without modifying the client core. Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -60,6 +60,7 @@ export const categorySectionFragment = graphql`
|
|||||||
description
|
description
|
||||||
kind
|
kind
|
||||||
gcmConsentTypes
|
gcmConsentTypes
|
||||||
|
posthogConsent
|
||||||
cookies(first: 100, orderBy: { field: CREATED_AT, direction: ASC })
|
cookies(first: 100, orderBy: { field: CREATED_AT, direction: ASC })
|
||||||
@connection(key: "CategorySection_cookies", filters: [])
|
@connection(key: "CategorySection_cookies", filters: [])
|
||||||
@required(action: THROW) {
|
@required(action: THROW) {
|
||||||
@@ -99,6 +100,7 @@ const updateCategoryMutation = graphql`
|
|||||||
description
|
description
|
||||||
rank
|
rank
|
||||||
gcmConsentTypes
|
gcmConsentTypes
|
||||||
|
posthogConsent
|
||||||
updatedAt
|
updatedAt
|
||||||
}
|
}
|
||||||
cookieBanner {
|
cookieBanner {
|
||||||
@@ -239,7 +241,10 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
|
|||||||
const cookies = category.cookies.edges.map(e => e.node);
|
const cookies = category.cookies.edges.map(e => e.node);
|
||||||
const isMutating = isUpdating || isCreating || isUpdatingCookie;
|
const isMutating = isUpdating || isCreating || isUpdatingCookie;
|
||||||
|
|
||||||
const handleSaveCategory = (name: string, slug: string, description: string, gcmConsentTypes: string[]) => {
|
const handleSaveCategory = (
|
||||||
|
name: string, slug: string, description: string,
|
||||||
|
gcmConsentTypes: string[], posthogConsent: boolean,
|
||||||
|
) => {
|
||||||
updateCategory({
|
updateCategory({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
@@ -248,6 +253,7 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
|
|||||||
slug,
|
slug,
|
||||||
description,
|
description,
|
||||||
gcmConsentTypes,
|
gcmConsentTypes,
|
||||||
|
posthogConsent,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onCompleted(_response, errors) {
|
onCompleted(_response, errors) {
|
||||||
@@ -485,6 +491,7 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
|
|||||||
slug={category.slug}
|
slug={category.slug}
|
||||||
description={category.description}
|
description={category.description}
|
||||||
gcmConsentTypes={[...category.gcmConsentTypes]}
|
gcmConsentTypes={[...category.gcmConsentTypes]}
|
||||||
|
posthogConsent={category.posthogConsent}
|
||||||
isUpdating={isUpdating}
|
isUpdating={isUpdating}
|
||||||
onSave={handleSaveCategory}
|
onSave={handleSaveCategory}
|
||||||
onCancel={() => setIsEditingCategory(false)}
|
onCancel={() => setIsEditingCategory(false)}
|
||||||
@@ -541,6 +548,16 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{category.posthogConsent && (
|
||||||
|
<div className="mt-2 flex items-center gap-1.5">
|
||||||
|
<span className="text-xs text-txt-secondary/70">
|
||||||
|
{__("PostHog:")}
|
||||||
|
</span>
|
||||||
|
<Badge variant="neutral">
|
||||||
|
{__("Tracking consent")}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,8 +31,9 @@ interface EditCategoryFormProps {
|
|||||||
slug: string;
|
slug: string;
|
||||||
description: string;
|
description: string;
|
||||||
gcmConsentTypes: string[];
|
gcmConsentTypes: string[];
|
||||||
|
posthogConsent: boolean;
|
||||||
isUpdating: boolean;
|
isUpdating: boolean;
|
||||||
onSave: (name: string, slug: string, description: string, gcmConsentTypes: string[]) => void;
|
onSave: (name: string, slug: string, description: string, gcmConsentTypes: string[], posthogConsent: boolean) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ export function EditCategoryForm({
|
|||||||
slug,
|
slug,
|
||||||
description,
|
description,
|
||||||
gcmConsentTypes,
|
gcmConsentTypes,
|
||||||
|
posthogConsent,
|
||||||
isUpdating,
|
isUpdating,
|
||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
@@ -50,6 +52,7 @@ export function EditCategoryForm({
|
|||||||
const [editSlug, setEditSlug] = useState(slug);
|
const [editSlug, setEditSlug] = useState(slug);
|
||||||
const [editDescription, setEditDescription] = useState(description);
|
const [editDescription, setEditDescription] = useState(description);
|
||||||
const [editGcmTypes, setEditGcmTypes] = useState<string[]>(gcmConsentTypes);
|
const [editGcmTypes, setEditGcmTypes] = useState<string[]>(gcmConsentTypes);
|
||||||
|
const [editPosthogConsent, setEditPosthogConsent] = useState(posthogConsent);
|
||||||
|
|
||||||
const toggleGcmType = (type: string) => {
|
const toggleGcmType = (type: string) => {
|
||||||
setEditGcmTypes(prev =>
|
setEditGcmTypes(prev =>
|
||||||
@@ -102,11 +105,28 @@ export function EditCategoryForm({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
{__("PostHog")}
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">
|
||||||
|
{__("Control PostHog tracking consent based on this category.")}
|
||||||
|
</p>
|
||||||
|
<label className="flex items-center gap-1.5 text-xs cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={editPosthogConsent}
|
||||||
|
onChange={() => setEditPosthogConsent(prev => !prev)}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<span>{__("Opt in/out of PostHog tracking")}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(editSlug)) return;
|
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(editSlug)) return;
|
||||||
onSave(editName, editSlug, editDescription, editGcmTypes);
|
onSave(editName, editSlug, editDescription, editGcmTypes, editPosthogConsent);
|
||||||
}}
|
}}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import { NotFoundError } from "./errors";
|
|||||||
import { fetchJSON } from "./http";
|
import { fetchJSON } from "./http";
|
||||||
import type { BannerTexts } from "./i18n";
|
import type { BannerTexts } from "./i18n";
|
||||||
import { detectLanguage } from "./i18n";
|
import { detectLanguage } from "./i18n";
|
||||||
|
import type { ConsentIntegration } from "./integrations";
|
||||||
|
import { createDefaultIntegrations } from "./integrations";
|
||||||
import { enqueue, flush } from "./queue";
|
import { enqueue, flush } from "./queue";
|
||||||
import { getOrCreateVisitorId } from "./visitor";
|
import { getOrCreateVisitorId } from "./visitor";
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ export interface Category {
|
|||||||
kind: string;
|
kind: string;
|
||||||
cookies: CookieItem[];
|
cookies: CookieItem[];
|
||||||
gcm_consent_types: string[];
|
gcm_consent_types: string[];
|
||||||
|
posthog_consent: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BannerConfig {
|
export interface BannerConfig {
|
||||||
@@ -84,6 +87,8 @@ export class CookieBannerClient {
|
|||||||
private readonly visitorId: string;
|
private readonly visitorId: string;
|
||||||
private readonly lang: string;
|
private readonly lang: string;
|
||||||
|
|
||||||
|
private readonly integrations: ConsentIntegration[];
|
||||||
|
|
||||||
private bannerConfig: BannerConfig | null = null;
|
private bannerConfig: BannerConfig | null = null;
|
||||||
private consent: VisitorConsent | null = null;
|
private consent: VisitorConsent | null = null;
|
||||||
private observer: MutationObserver | null = null;
|
private observer: MutationObserver | null = null;
|
||||||
@@ -99,6 +104,7 @@ export class CookieBannerClient {
|
|||||||
this.bannerId = config.bannerId;
|
this.bannerId = config.bannerId;
|
||||||
this.visitorId = getOrCreateVisitorId(config.bannerId);
|
this.visitorId = getOrCreateVisitorId(config.bannerId);
|
||||||
this.lang = detectLanguage(config.lang);
|
this.lang = detectLanguage(config.lang);
|
||||||
|
this.integrations = createDefaultIntegrations();
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(): Promise<void> {
|
async load(): Promise<void> {
|
||||||
@@ -109,7 +115,9 @@ export class CookieBannerClient {
|
|||||||
const config = await fetchJSON<BannerConfig>(configUrl);
|
const config = await fetchJSON<BannerConfig>(configUrl);
|
||||||
this.bannerConfig = config;
|
this.bannerConfig = config;
|
||||||
|
|
||||||
this.setGoogleConsentDefaults();
|
for (const integration of this.integrations) {
|
||||||
|
integration.setDefaults(config.categories);
|
||||||
|
}
|
||||||
this.startDetector(config);
|
this.startDetector(config);
|
||||||
|
|
||||||
const cookie = getConsentCookie();
|
const cookie = getConsentCookie();
|
||||||
@@ -274,7 +282,9 @@ export class CookieBannerClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private activate(consentData: Record<string, boolean>): void {
|
private activate(consentData: Record<string, boolean>): void {
|
||||||
this.updateGoogleConsentMode(consentData);
|
for (const integration of this.integrations) {
|
||||||
|
integration.update(this.config.categories, consentData);
|
||||||
|
}
|
||||||
|
|
||||||
const categoryCookies: Record<string, string[]> = {};
|
const categoryCookies: Record<string, string[]> = {};
|
||||||
const categoryLabels: Record<string, string> = {};
|
const categoryLabels: Record<string, string> = {};
|
||||||
@@ -293,55 +303,6 @@ export class CookieBannerClient {
|
|||||||
this.observer = observeAndActivate(consentData, categoryLabels, texts);
|
this.observer = observeAndActivate(consentData, categoryLabels, texts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasGCMMapping(): boolean {
|
|
||||||
return this.config.categories.some(
|
|
||||||
(cat) => cat.gcm_consent_types && cat.gcm_consent_types.length > 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setGoogleConsentDefaults(): void {
|
|
||||||
if (!this.hasGCMMapping()) return;
|
|
||||||
|
|
||||||
const w = window as unknown as Record<string, unknown>;
|
|
||||||
const gtag = w.gtag as ((...args: unknown[]) => void) | undefined;
|
|
||||||
if (typeof gtag !== "function") return;
|
|
||||||
|
|
||||||
const defaults: Record<string, string> = {};
|
|
||||||
for (const cat of this.config.categories) {
|
|
||||||
if (!cat.gcm_consent_types) continue;
|
|
||||||
for (const gcmType of cat.gcm_consent_types) {
|
|
||||||
defaults[gcmType] = "denied";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(defaults).length > 0) {
|
|
||||||
gtag("consent", "default", defaults);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateGoogleConsentMode(
|
|
||||||
consentData: Record<string, boolean>,
|
|
||||||
): void {
|
|
||||||
if (!this.hasGCMMapping()) return;
|
|
||||||
|
|
||||||
const w = window as unknown as Record<string, unknown>;
|
|
||||||
const gtag = w.gtag as ((...args: unknown[]) => void) | undefined;
|
|
||||||
if (typeof gtag !== "function") return;
|
|
||||||
|
|
||||||
const update: Record<string, string> = {};
|
|
||||||
for (const cat of this.config.categories) {
|
|
||||||
if (!cat.gcm_consent_types) continue;
|
|
||||||
const granted = !!consentData[cat.slug];
|
|
||||||
for (const gcmType of cat.gcm_consent_types) {
|
|
||||||
update[gcmType] = granted ? "granted" : "denied";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(update).length > 0) {
|
|
||||||
gtag("consent", "update", update);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private startDetector(config: BannerConfig): void {
|
private startDetector(config: BannerConfig): void {
|
||||||
const knownNames = new Set<string>();
|
const knownNames = new Set<string>();
|
||||||
knownNames.add(COOKIE_NAME);
|
knownNames.add(COOKIE_NAME);
|
||||||
|
|||||||
73
packages/cookie-banner/src/integrations/gcm.ts
Normal file
73
packages/cookie-banner/src/integrations/gcm.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import type { Category } from "../client";
|
||||||
|
import type { ConsentIntegration } from "./integration";
|
||||||
|
|
||||||
|
export class GoogleConsentModeIntegration implements ConsentIntegration {
|
||||||
|
private hasMapping(categories: Category[]): boolean {
|
||||||
|
return categories.some(
|
||||||
|
(cat) => cat.gcm_consent_types && cat.gcm_consent_types.length > 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getGtag(): ((...args: unknown[]) => void) | null {
|
||||||
|
const w = window as unknown as Record<string, unknown>;
|
||||||
|
const gtag = w.gtag as ((...args: unknown[]) => void) | undefined;
|
||||||
|
if (typeof gtag !== "function") return null;
|
||||||
|
return gtag;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaults(categories: Category[]): void {
|
||||||
|
if (!this.hasMapping(categories)) return;
|
||||||
|
|
||||||
|
const gtag = this.getGtag();
|
||||||
|
if (!gtag) return;
|
||||||
|
|
||||||
|
const defaults: Record<string, string> = {};
|
||||||
|
for (const cat of categories) {
|
||||||
|
if (!cat.gcm_consent_types) continue;
|
||||||
|
for (const gcmType of cat.gcm_consent_types) {
|
||||||
|
defaults[gcmType] = "denied";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(defaults).length > 0) {
|
||||||
|
gtag("consent", "default", defaults);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(
|
||||||
|
categories: Category[],
|
||||||
|
consentData: Record<string, boolean>,
|
||||||
|
): void {
|
||||||
|
if (!this.hasMapping(categories)) return;
|
||||||
|
|
||||||
|
const gtag = this.getGtag();
|
||||||
|
if (!gtag) return;
|
||||||
|
|
||||||
|
const update: Record<string, string> = {};
|
||||||
|
for (const cat of categories) {
|
||||||
|
if (!cat.gcm_consent_types) continue;
|
||||||
|
const granted = !!consentData[cat.slug];
|
||||||
|
for (const gcmType of cat.gcm_consent_types) {
|
||||||
|
update[gcmType] = granted ? "granted" : "denied";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(update).length > 0) {
|
||||||
|
gtag("consent", "update", update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
packages/cookie-banner/src/integrations/index.ts
Normal file
28
packages/cookie-banner/src/integrations/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
export type { ConsentIntegration } from "./integration";
|
||||||
|
export { GoogleConsentModeIntegration } from "./gcm";
|
||||||
|
export { PostHogIntegration } from "./posthog";
|
||||||
|
|
||||||
|
import type { ConsentIntegration } from "./integration";
|
||||||
|
import { GoogleConsentModeIntegration } from "./gcm";
|
||||||
|
import { PostHogIntegration } from "./posthog";
|
||||||
|
|
||||||
|
export function createDefaultIntegrations(): ConsentIntegration[] {
|
||||||
|
return [
|
||||||
|
new GoogleConsentModeIntegration(),
|
||||||
|
new PostHogIntegration(),
|
||||||
|
];
|
||||||
|
}
|
||||||
23
packages/cookie-banner/src/integrations/integration.ts
Normal file
23
packages/cookie-banner/src/integrations/integration.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import type { Category } from "../client";
|
||||||
|
|
||||||
|
export interface ConsentIntegration {
|
||||||
|
/** Called once after config is loaded, before any consent is applied. */
|
||||||
|
setDefaults(categories: Category[]): void;
|
||||||
|
|
||||||
|
/** Called whenever consent changes (accept, reject, customize, GPC). */
|
||||||
|
update(categories: Category[], consentData: Record<string, boolean>): void;
|
||||||
|
}
|
||||||
69
packages/cookie-banner/src/integrations/posthog.ts
Normal file
69
packages/cookie-banner/src/integrations/posthog.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import type { Category } from "../client";
|
||||||
|
import type { ConsentIntegration } from "./integration";
|
||||||
|
|
||||||
|
interface PostHogInstance {
|
||||||
|
opt_in_capturing: () => void;
|
||||||
|
opt_out_capturing: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PostHogIntegration implements ConsentIntegration {
|
||||||
|
private hasMapping(categories: Category[]): boolean {
|
||||||
|
return categories.some((cat) => cat.posthog_consent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPostHog(): PostHogInstance | null {
|
||||||
|
const w = window as unknown as Record<string, unknown>;
|
||||||
|
const posthog = w.posthog as (PostHogInstance & Record<string, unknown>) | undefined;
|
||||||
|
if (
|
||||||
|
!posthog ||
|
||||||
|
typeof posthog.opt_in_capturing !== "function" ||
|
||||||
|
typeof posthog.opt_out_capturing !== "function"
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return posthog;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaults(categories: Category[]): void {
|
||||||
|
if (!this.hasMapping(categories)) return;
|
||||||
|
|
||||||
|
const posthog = this.getPostHog();
|
||||||
|
if (!posthog) return;
|
||||||
|
|
||||||
|
posthog.opt_out_capturing();
|
||||||
|
}
|
||||||
|
|
||||||
|
update(
|
||||||
|
categories: Category[],
|
||||||
|
consentData: Record<string, boolean>,
|
||||||
|
): void {
|
||||||
|
if (!this.hasMapping(categories)) return;
|
||||||
|
|
||||||
|
const posthog = this.getPostHog();
|
||||||
|
if (!posthog) return;
|
||||||
|
|
||||||
|
const granted = categories
|
||||||
|
.filter((cat) => cat.posthog_consent)
|
||||||
|
.every((cat) => !!consentData[cat.slug]);
|
||||||
|
|
||||||
|
if (granted) {
|
||||||
|
posthog.opt_in_capturing();
|
||||||
|
} else {
|
||||||
|
posthog.opt_out_capturing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,6 +74,7 @@ type (
|
|||||||
Slug *string
|
Slug *string
|
||||||
Description *string
|
Description *string
|
||||||
GCMConsentTypes *[]string
|
GCMConsentTypes *[]string
|
||||||
|
PostHogConsent *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
CreateCookieRequest struct {
|
CreateCookieRequest struct {
|
||||||
@@ -360,6 +361,7 @@ func buildSnapshot(
|
|||||||
Kind: c.Kind,
|
Kind: c.Kind,
|
||||||
Cookies: cookies,
|
Cookies: cookies,
|
||||||
GCMConsentTypes: gcmConsentTypes,
|
GCMConsentTypes: gcmConsentTypes,
|
||||||
|
PostHogConsent: c.PostHogConsent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1347,6 +1349,9 @@ func (s *Service) UpdateCookieCategory(
|
|||||||
if req.GCMConsentTypes != nil {
|
if req.GCMConsentTypes != nil {
|
||||||
category.GCMConsentTypes = *req.GCMConsentTypes
|
category.GCMConsentTypes = *req.GCMConsentTypes
|
||||||
}
|
}
|
||||||
|
if req.PostHogConsent != nil {
|
||||||
|
category.PostHogConsent = *req.PostHogConsent
|
||||||
|
}
|
||||||
|
|
||||||
category.UpdatedAt = time.Now()
|
category.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ type (
|
|||||||
Kind CookieCategoryKind `json:"kind"`
|
Kind CookieCategoryKind `json:"kind"`
|
||||||
Cookies CookieItems `json:"cookies"`
|
Cookies CookieItems `json:"cookies"`
|
||||||
GCMConsentTypes []string `json:"gcm_consent_types"`
|
GCMConsentTypes []string `json:"gcm_consent_types"`
|
||||||
|
PostHogConsent bool `json:"posthog_consent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
CookieBannerVersion struct {
|
CookieBannerVersion struct {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ type (
|
|||||||
Kind CookieCategoryKind `db:"kind"`
|
Kind CookieCategoryKind `db:"kind"`
|
||||||
Rank int `db:"rank"`
|
Rank int `db:"rank"`
|
||||||
GCMConsentTypes []string `db:"gcm_consent_types"`
|
GCMConsentTypes []string `db:"gcm_consent_types"`
|
||||||
|
PostHogConsent bool `db:"posthog_consent"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -111,6 +112,7 @@ SELECT
|
|||||||
kind,
|
kind,
|
||||||
rank,
|
rank,
|
||||||
gcm_consent_types,
|
gcm_consent_types,
|
||||||
|
posthog_consent,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -162,6 +164,7 @@ SELECT
|
|||||||
kind,
|
kind,
|
||||||
rank,
|
rank,
|
||||||
gcm_consent_types,
|
gcm_consent_types,
|
||||||
|
posthog_consent,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -241,6 +244,7 @@ SELECT
|
|||||||
kind,
|
kind,
|
||||||
rank,
|
rank,
|
||||||
gcm_consent_types,
|
gcm_consent_types,
|
||||||
|
posthog_consent,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -289,6 +293,7 @@ INSERT INTO cookie_categories (
|
|||||||
kind,
|
kind,
|
||||||
rank,
|
rank,
|
||||||
gcm_consent_types,
|
gcm_consent_types,
|
||||||
|
posthog_consent,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
@@ -302,6 +307,7 @@ INSERT INTO cookie_categories (
|
|||||||
@kind,
|
@kind,
|
||||||
@rank,
|
@rank,
|
||||||
@gcm_consent_types,
|
@gcm_consent_types,
|
||||||
|
@posthog_consent,
|
||||||
@created_at,
|
@created_at,
|
||||||
@updated_at
|
@updated_at
|
||||||
)
|
)
|
||||||
@@ -318,6 +324,7 @@ INSERT INTO cookie_categories (
|
|||||||
"kind": c.Kind,
|
"kind": c.Kind,
|
||||||
"rank": c.Rank,
|
"rank": c.Rank,
|
||||||
"gcm_consent_types": c.GCMConsentTypes,
|
"gcm_consent_types": c.GCMConsentTypes,
|
||||||
|
"posthog_consent": c.PostHogConsent,
|
||||||
"created_at": c.CreatedAt,
|
"created_at": c.CreatedAt,
|
||||||
"updated_at": c.UpdatedAt,
|
"updated_at": c.UpdatedAt,
|
||||||
}
|
}
|
||||||
@@ -347,6 +354,7 @@ SET
|
|||||||
slug = @slug,
|
slug = @slug,
|
||||||
description = @description,
|
description = @description,
|
||||||
gcm_consent_types = @gcm_consent_types,
|
gcm_consent_types = @gcm_consent_types,
|
||||||
|
posthog_consent = @posthog_consent,
|
||||||
updated_at = @updated_at
|
updated_at = @updated_at
|
||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
@@ -361,6 +369,7 @@ WHERE
|
|||||||
"slug": c.Slug,
|
"slug": c.Slug,
|
||||||
"description": c.Description,
|
"description": c.Description,
|
||||||
"gcm_consent_types": c.GCMConsentTypes,
|
"gcm_consent_types": c.GCMConsentTypes,
|
||||||
|
"posthog_consent": c.PostHogConsent,
|
||||||
"updated_at": c.UpdatedAt,
|
"updated_at": c.UpdatedAt,
|
||||||
}
|
}
|
||||||
maps.Copy(args, scope.SQLArguments())
|
maps.Copy(args, scope.SQLArguments())
|
||||||
@@ -477,6 +486,7 @@ SELECT
|
|||||||
kind,
|
kind,
|
||||||
rank,
|
rank,
|
||||||
gcm_consent_types,
|
gcm_consent_types,
|
||||||
|
posthog_consent,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
|
|||||||
16
pkg/coredata/migrations/20260424T112802Z.sql
Normal file
16
pkg/coredata/migrations/20260424T112802Z.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
--
|
||||||
|
-- Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
-- purpose with or without fee is hereby granted, provided that the above
|
||||||
|
-- copyright notice and this permission notice appear in all copies.
|
||||||
|
--
|
||||||
|
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
-- PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
ALTER TABLE cookie_categories ADD COLUMN posthog_consent BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
ALTER TABLE cookie_categories ALTER COLUMN posthog_consent DROP DEFAULT;
|
||||||
@@ -472,6 +472,7 @@ func (r *mutationResolver) UpdateCookieCategory(ctx context.Context, input types
|
|||||||
Slug: input.Slug,
|
Slug: input.Slug,
|
||||||
Description: input.Description,
|
Description: input.Description,
|
||||||
GCMConsentTypes: gcmConsentTypes,
|
GCMConsentTypes: gcmConsentTypes,
|
||||||
|
PostHogConsent: input.PosthogConsent,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ type CookieCategory implements Node {
|
|||||||
kind: CookieCategoryKind!
|
kind: CookieCategoryKind!
|
||||||
rank: Int!
|
rank: Int!
|
||||||
gcmConsentTypes: [String!]!
|
gcmConsentTypes: [String!]!
|
||||||
|
posthogConsent: Boolean!
|
||||||
|
|
||||||
cookies(
|
cookies(
|
||||||
first: Int
|
first: Int
|
||||||
@@ -310,6 +311,7 @@ input UpdateCookieCategoryInput {
|
|||||||
slug: String
|
slug: String
|
||||||
description: String
|
description: String
|
||||||
gcmConsentTypes: [String!]
|
gcmConsentTypes: [String!]
|
||||||
|
posthogConsent: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
input DeleteCookieCategoryInput {
|
input DeleteCookieCategoryInput {
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ func NewCookieCategory(c *coredata.CookieCategory) *CookieCategory {
|
|||||||
Kind: c.Kind,
|
Kind: c.Kind,
|
||||||
Rank: c.Rank,
|
Rank: c.Rank,
|
||||||
GcmConsentTypes: gcmConsentTypes,
|
GcmConsentTypes: gcmConsentTypes,
|
||||||
|
PosthogConsent: c.PostHogConsent,
|
||||||
CreatedAt: c.CreatedAt,
|
CreatedAt: c.CreatedAt,
|
||||||
UpdatedAt: c.UpdatedAt,
|
UpdatedAt: c.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user