Skip to content

add Google Analytics with consent banner#90

Merged
andymeierdev merged 1 commit intomainfrom
andymeierdev/add-google-analytics-consent
Apr 15, 2026
Merged

add Google Analytics with consent banner#90
andymeierdev merged 1 commit intomainfrom
andymeierdev/add-google-analytics-consent

Conversation

@andymeierdev
Copy link
Copy Markdown
Collaborator

Summary

  • add Google Analytics configuration to the app and Pulumi deployment
  • require the GA measurement ID in app and infra config
  • add an EU-safe consent banner that defaults analytics to denied and loads GA only after acceptance
  • prevent article link full-page navigation flashes when Datastar handles navigation
  • add view tests covering GA + consent banner rendering

Verification

  • cd app && ./fake.sh Test
  • cd pulumi && npm run check
  • cd pulumi && pulumi preview (requires stack config/environment wiring for googleAnalytics:measurementId)

@github-actions
Copy link
Copy Markdown

🍹 preview on andymeier/prod

Pulumi report

View in Pulumi Cloud

  Previewing update (prod)

View Live: https://app.pulumi.com/meiermade/andymeier/prod/previews/7b2a5d05-f38a-410a-a0d8-87d2df4a588f

pulumi:pulumi:Stack: (same)
  [urn=urn:pulumi:prod::andymeier::pulumi:pulumi:Stack::andymeier-prod]
  ~ docker-build:index:Image: (update)
      [id=sha256:ce2a92a5ac3610bf75a8e4d0bd3af7000f4199af0b2612252179582b79ae116c]
      [urn=urn:pulumi:prod::andymeier::docker-build:index:Image::andymeier]
    ~ context    : {
        ~ location: "/Users/andy/repos/meiermade/andymeier/app" => "/home/runner/work/andymeier/andymeier/app"
      }
    - contextHash: "09622a741bc3ff4095f54ba79ec989879c7e0b94057e945b445deba89f2c850e"
    ~ dockerfile : {
        ~ location: "/Users/andy/repos/meiermade/andymeier/app/Dockerfile" => "/home/runner/work/andymeier/andymeier/app/Dockerfile"
      }
  +-kubernetes:core/v1:Secret: (replace)
      [id=andymeier/app]
      [urn=urn:pulumi:prod::andymeier::kubernetes:core/v1:Secret::app]
    ~ data: {
        + GOOGLE_ANALYTICS_MEASUREMENT_ID: [secret]
      }
  +-kubernetes:apps/v1:Deployment: (replace)
      [id=andymeier/app]
      [urn=urn:pulumi:prod::andymeier::kubernetes:apps/v1:Deployment::app]
      apiVersion: "apps/v1"
      kind      : "Deployment"
      metadata  : {
          name     : "app"
          namespace: "andymeier"
      }
    ~ spec      : {
          replicas: 1
          selector: {
              matchLabels: {
                  app.kubernetes.io/name: "app"
              }
          }
        ~ template: {
              metadata: {
                  labels: {
                      app.kubernetes.io/name: "app"
                  }
              }
            ~ spec    : {
                ~ containers     : [
                    ~ [0]: {
                            ~ envFrom        : [
                                ~ [0]: {
                                        ~ secretRef: {
                                            ~ name: "app" => "appwspp4"
                                          }
                                      }
                              ]
                            ~ image          : "us-east1-docker.pkg.dev/meiermade-platform/platform/andymeier:latest@sha256:ce2a92a5ac3610bf75a8e4d0bd3af7000f4199af0b2612252179582b79ae116c" => [unknown]
                              imagePullPolicy: "IfNotPresent"
                              livenessProbe  : {
                                  httpGet            : {
                                      path: "/health"
                                      port: 5000
                                  }
                                  initialDelaySeconds: 5
                              }
                              name           : "app"
                              readinessProbe : {
                                  httpGet            : {
                                      path: "/health"
                                      port: 5000
                                  }
                                  initialDelaySeconds: 5
                              }
                              resources      : {
                                  limits  : {
                                      cpu   : "250m"
                                      memory: "256Mi"
                                  }
                                  requests: {
                                      cpu   : "25m"
                                      memory: "64Mi"
                                  }
                              }
                              securityContext: {
                                  allowPrivilegeEscalation: false
                                  capabilities            : {
                                      drop: [
                                          [0]: "ALL"
                                      ]
                                  }
                              }
                              volumeMounts   : [
                                  [0]: {
                                      mountPath: "/data"
                                      name     : "app-data"
                                  }
                              ]
                          }
                      [1]: {
                              args           : [
                                  [0]: "tunnel"
                                  [1]: "--no-autoupdate"
                                  [2]: "run"
                              ]
                              envFrom        : [
                                  [0]: {
                                      secretRef: {
                                          name: "cloudflared"
                                      }
                                  }
                              ]
                              image          : "cloudflare/cloudflared:2026.2.0"
                              livenessProbe  : {
                                  failureThreshold   : 1
                                  httpGet            : {
                                      path: "/ready"
                                      port: 2000
                                  }
                                  initialDelaySeconds: 10
                                  periodSeconds      : 10
                              }
                              name           : "cloudflared"
                              resources      : {
                                  limits  : {
                                      cpu   : "100m"
                                      memory: "128Mi"
                                  }
                                  requests: {
                                      cpu   : "10m"
                                      memory: "32Mi"
                                  }
                              }
                              securityContext: {
                                  allowPrivilegeEscalation: false
                                  capabilities            : {
                                      drop: [
                                          [0]: "ALL"
                                      ]
                                  }
                              }
                          }
                  ]
                  securityContext: {
                      runAsNonRoot  : true
                      seccompProfile: {
                          type: "RuntimeDefault"
                      }
                  }
                  volumes        : [
                      [0]: {
                          emptyDir: {}
                          name    : "app-data"
                      }
                  ]
              }
          }
      }
Resources:
  ~ 1 to update
  +-2 to replace
  3 changes. 13 unchanged
  

@andymeierdev andymeierdev merged commit d6cdfe2 into main Apr 15, 2026
2 checks passed
@andymeierdev andymeierdev deleted the andymeierdev/add-google-analytics-consent branch April 15, 2026 14:44
Copy link
Copy Markdown

@minniemeierdev minniemeierdev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation looks solid. Tests pass, the cookie banner handles consent well and correctly persists via localStorage. Using Pulumi correctly to pass the config too. I love the fix to prevent default click navigation on article cards as well!

One thing to keep in mind for future PRs: since Datastar allows navigating without full page reloads, gtag('config', '<id>') is only firing on the initial page load. Google Analytics won't natively track subsequent page navigations over Datastar swaps unless you emit a manual page_view event to gtag on Datastar swap events or URL changes. For now this gets tracking up and running!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants