Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ CRON_SECRET=""
# Get your keys from: https://posthog.com
NEXT_PUBLIC_POSTHOG_KEY=""
NEXT_PUBLIC_POSTHOG_HOST="https://eu.i.posthog.com"
# Optional override; Docker builds generate one automatically when omitted
NEXT_PUBLIC_APP_VERSION=""

# -----------------------------------------
# Umami Analytics (Required)
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ jobs:
platforms: linux/amd64
load: true
tags: louez-web:smoke
build-args: |
NEXT_PUBLIC_APP_VERSION=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

Expand Down Expand Up @@ -175,5 +177,7 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
NEXT_PUBLIC_APP_VERSION=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
2 changes: 2 additions & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ CRON_SECRET=""
# Get your keys from: https://posthog.com
NEXT_PUBLIC_POSTHOG_KEY=""
NEXT_PUBLIC_POSTHOG_HOST="https://eu.i.posthog.com"
# Optional override; Docker builds generate one automatically when omitted
NEXT_PUBLIC_APP_VERSION=""

# -----------------------------------------
# Umami Analytics (Required)
Expand Down
11 changes: 2 additions & 9 deletions apps/web/app/(storefront)/[slug]/rental/rental-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
} from "@/lib/utils/duration";

import { useStorefrontUrl } from "@/hooks/use-storefront-url";
import { useBrowserTimezoneCity } from "@/hooks/use-browser-timezone-city";

import { useCart } from "@/contexts/cart-context";

Expand Down Expand Up @@ -208,15 +209,7 @@ export function RentalContent({
[endDate, storeTimezone],
);

// Detect if user's browser timezone differs from the store's timezone
const timezoneCity = useMemo(() => {
if (!storeTimezone) return null;
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (browserTimezone === storeTimezone) return null;
// Extract city name from IANA timezone (e.g., "Europe/Paris" → "Paris")
const city = storeTimezone.split("/").pop()?.replace(/_/g, " ");
return city || storeTimezone;
}, [storeTimezone]);
const timezoneCity = useBrowserTimezoneCity(storeTimezone);

// Set global dates in cart context
useEffect(() => {
Expand Down
9 changes: 2 additions & 7 deletions apps/web/components/storefront/date-selection-hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@louez/ui";
import { ScrollArea } from "@louez/ui";
import { cn } from "@louez/utils";
import { useCart } from "@/contexts/cart-context";
import { useBrowserTimezoneCity } from "@/hooks/use-browser-timezone-city";
import { useStorefrontUrl } from "@/hooks/use-storefront-url";
import { getMinStartDate, isTimeSlotAvailable, type PricingMode } from "@/lib/utils/duration";
import { isCalendarDateBeforeSelectedDate } from "@/components/storefront/date-picker/core/use-rental-date-core";
Expand Down Expand Up @@ -332,13 +333,7 @@ export function DateSelectionHero({
return `${days} ${days > 1 ? t("durationDays") : t("durationDay")} ${t("and")} ${hours}h`;
};

const timezoneCity = useMemo(() => {
if (!timezone) return null;
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (browserTimezone === timezone) return null;
const city = timezone.split("/").pop()?.replace(/_/g, " ");
return city || timezone;
}, [timezone]);
const timezoneCity = useBrowserTimezoneCity(timezone);

// Check if we can submit - always require times
const canSubmit = startDate && endDate && startTime && endTime;
Expand Down
9 changes: 2 additions & 7 deletions apps/web/components/storefront/embed-date-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
isCalendarDateBeforeSelectedDate,
useRentalDateCore,
} from '@/components/storefront/date-picker/core/use-rental-date-core'
import { useBrowserTimezoneCity } from '@/hooks/use-browser-timezone-city'

// ─── Types ────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -344,13 +345,7 @@ export function EmbedDatePicker({
tEmbed,
])

const timezoneCity = useMemo(() => {
if (!timezone) return null
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
if (browserTimezone === timezone) return null
const city = timezone.split('/').pop()?.replace(/_/g, ' ')
return city || timezone
}, [timezone])
const timezoneCity = useBrowserTimezoneCity(timezone)

const handleSubmit = () => {
if (validationError) {
Expand Down
9 changes: 2 additions & 7 deletions apps/web/components/storefront/hero-date-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@louez/ui";
import { ScrollArea } from "@louez/ui";
import { cn } from "@louez/utils";
import { useCart } from "@/contexts/cart-context";
import { useBrowserTimezoneCity } from "@/hooks/use-browser-timezone-city";
import { useStorefrontUrl } from "@/hooks/use-storefront-url";
import { type PricingMode } from "@/lib/utils/duration";
import {
Expand Down Expand Up @@ -267,13 +268,7 @@ export function HeroDatePicker({
return true;
}, [startDate, endDate, startTime, endTime, isSameDay, minRentalMinutes, timezone]);

const timezoneCity = useMemo(() => {
if (!timezone) return null;
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (browserTimezone === timezone) return null;
const city = timezone.split("/").pop()?.replace(/_/g, " ");
return city || timezone;
}, [timezone]);
const timezoneCity = useBrowserTimezoneCity(timezone);

const durationWarning = useMemo(() => {
if (!startDate || !endDate || !startTime || !endTime) return null;
Expand Down
2 changes: 2 additions & 0 deletions apps/web/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export const env = createEnv({
.string()
.min(1, 'NEXT_PUBLIC_POSTHOG_KEY is required'),
NEXT_PUBLIC_POSTHOG_HOST: z.url().default('https://eu.i.posthog.com'),
NEXT_PUBLIC_APP_VERSION: z.string().min(1).max(100).optional(),

// ===== Umami Analytics (Required) =====
NEXT_PUBLIC_UMAMI_SCRIPT_URL: z
Expand Down Expand Up @@ -324,6 +325,7 @@ export const env = createEnv({
NEXT_PUBLIC_VAPID_PUBLIC_KEY: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION,
NEXT_PUBLIC_UMAMI_SCRIPT_URL: process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
NEXT_PUBLIC_GLEAP_API_KEY: process.env.NEXT_PUBLIC_GLEAP_API_KEY,
Expand Down
25 changes: 25 additions & 0 deletions apps/web/hooks/use-browser-timezone-city.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";

import { useEffect, useState } from "react";

export function useBrowserTimezoneCity(timezone?: string): string | null {
const [timezoneCity, setTimezoneCity] = useState<string | null>(null);

useEffect(() => {
if (!timezone) {
setTimezoneCity(null);
return;
}

const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;

if (browserTimezone === timezone) {
setTimezoneCity(null);
return;
}

setTimezoneCity(timezone.split("/").pop()?.replace(/_/g, " ") || timezone);
}, [timezone]);

return timezoneCity;
}
11 changes: 8 additions & 3 deletions apps/web/instrumentation-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@

import posthog from 'posthog-js'

import { env } from '@/env'

function resolveSurface(): { isDashboard: boolean; storeSlug: string | null } {
const hostname = window.location.hostname.toLowerCase()
const baseDomain = (process.env.NEXT_PUBLIC_APP_DOMAIN || 'localhost:3000')
const baseDomain = (env.NEXT_PUBLIC_APP_DOMAIN || 'localhost:3000')
.split(':')[0]
.toLowerCase()

Expand All @@ -45,10 +47,10 @@ function resolveSurface(): { isDashboard: boolean; storeSlug: string | null } {
return { isDashboard: false, storeSlug: null }
}

if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
if (typeof window !== 'undefined' && env.NEXT_PUBLIC_POSTHOG_KEY) {
const { isDashboard, storeSlug } = resolveSurface()

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, {
// Route analytics through our reverse proxy to avoid CORS and ad blockers
// The proxy is configured in next.config.ts rewrites
api_host: '/ingest',
Expand Down Expand Up @@ -95,6 +97,9 @@ if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
posthog.register({
surface: isDashboard ? 'dashboard' : storeSlug ? 'storefront' : 'marketing',
...(storeSlug ? { store_slug: storeSlug } : {}),
...(env.NEXT_PUBLIC_APP_VERSION
? { $app_version: env.NEXT_PUBLIC_APP_VERSION }
: {}),
})
}

Expand Down
8 changes: 6 additions & 2 deletions docker/Dockerfile.web
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ ARG DATABASE_URL
ARG NEXT_PUBLIC_APP_URL
ARG NEXT_PUBLIC_APP_DOMAIN
ARG NEXT_PUBLIC_DASHBOARD_SUBDOMAIN
ARG NEXT_PUBLIC_APP_VERSION
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
ARG NEXT_PUBLIC_POSTHOG_KEY
ARG NEXT_PUBLIC_POSTHOG_HOST
Expand Down Expand Up @@ -114,8 +115,11 @@ ENV NEXT_PUBLIC_FROMHELLO_API_URL=${NEXT_PUBLIC_FROMHELLO_API_URL}
ENV NEXT_PUBLIC_FROMHELLO_KEY=${NEXT_PUBLIC_FROMHELLO_KEY}
ENV NEXT_PUBLIC_FROMHELLO_COOKIE_DOMAIN=${NEXT_PUBLIC_FROMHELLO_COOKIE_DOMAIN}

# Build the application using Turborepo
RUN corepack enable pnpm && pnpm turbo run build --filter=@louez/web --env-mode=loose
# Build the application using Turborepo. CI supplies the commit SHA; direct
# EasyPanel builds fall back to a unique build identifier without manual input.
RUN corepack enable pnpm && \
APP_VERSION="${NEXT_PUBLIC_APP_VERSION:-build-$(date -u +%Y%m%d%H%M%S)}" && \
NEXT_PUBLIC_APP_VERSION="${APP_VERSION}" pnpm turbo run build --filter=@louez/web --env-mode=loose

# -----------------------------------------------------------------------------
# Stage 3: Migrator
Expand Down
1 change: 1 addition & 0 deletions docs/analytics/setup-review-2026-07.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Instrumentation code (`apps/web`) :
- **Fix `/ingest` sur les storefronts** : `proxy.ts` reecrivait `/ingest/*` vers `/{slug}/ingest/*` sur les sous-domaines boutique, donc chaque appel posthog-js storefront finissait en 404. `/ingest` passe maintenant en pass-through comme `/api` — c'est la cause racine du "zero pageview storefront".
- **Config storefront exemptee de consentement** (`instrumentation-client.ts`) : init host-aware. Dashboard (`app.{domaine}`) = config complete (cookies scoped au host via `cross_subdomain_cookie: false`, replay, autocapture). Storefronts (`{slug}.{domaine}`) et pages marketing = mesure d'audience exemptee CNIL : `persistence: memory` (aucun cookie/localStorage), events anonymes (`person_profiles: identified_only`), pas d'autocapture, pas de replay, pas de heatmaps, pas de dead clicks, pas de surveys. Super props `surface` (`storefront` | `marketing`) et `store_slug` enregistrees a chaque chargement. **Elargir une de ces options cote storefront = banniere de consentement obligatoire.**
- **Surface dashboard explicite** (`instrumentation-client.ts`) : a partir du prochain deploy apres le 2026-07-03, les pageviews dashboard portent aussi `surface: dashboard`. Les pageviews dashboard historiques restent souvent `surface = unknown`.
- **Version applicative sur les events client** (`instrumentation-client.ts`) : PostHog recoit `$app_version` sur les events client pour relier les exceptions a une version. Le workflow GitHub fournit le SHA exact du commit ; un build Docker direct (dont EasyPanel) genere automatiquement un identifiant `build-YYYYMMDDHHMMSS` si aucune version n'est fournie. `NEXT_PUBLIC_APP_VERSION` reste disponible comme override optionnel.

## Roadmap des manques (prioritee)

Expand Down
Loading