Skip to content

Latest commit

 

History

History
75 lines (52 loc) · 6.92 KB

File metadata and controls

75 lines (52 loc) · 6.92 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

A Looker Studio Community Connector (Apps Script project) that bridges a self-hosted Plausible Community Edition instance to Looker Studio via Plausible's Stats API v2 (POST /api/v2/query). It is not a Node project — there is no package.json, no test runner, no linter. Source lives in flat .gs files at the repo root.

Working with the project

There is no local build, test, or lint step. The project runs only inside Google's Apps Script environment.

# Push changes to the linked Apps Script project (after `clasp login` + filling .clasp.json scriptId)
clasp push

# Open the Apps Script editor in the browser
clasp open

# First-time setup (creates the Apps Script project and writes scriptId into .clasp.json)
clasp create --type standalone --title "Plausible CE Connector" --rootDir .

To verify changes: in the Apps Script editor → Bereitstellen → Test-Bereitstellungen → Looker Studio, then exercise auth/config/charts in the resulting Looker Studio data source. The verification scenarios listed in the README's filter-mapping section (EQUALS, IN_LIST EXCLUDE, CONTAINS, custom-property filter, goal filter) are the canonical end-to-end checks.

.claspignore whitelists exactly the eight source files that get pushed — anything else (README, plan files, this file) stays local.

Architecture

Apps Script runtime model (matters more than it looks)

All .gs files share a single global scope when Apps Script loads them — file boundaries are organizational only. Functions defined in Util.gs are directly callable from Code.gs without any import. This is why the codebase has no require/import.

The Looker Studio framework calls a fixed set of top-level entry-point functions by exact name: getAuthType, getConfig, getSchema, getData, isAdminUser, setCredentials, resetAuth, isAuthValid. These live in Code.gs and Auth.gs. Renaming any of them silently breaks the connector. Everything else in the codebase uses a trailing-underscore convention (loadCredentials_, translateFilters_, plausibleQuery_) to mark "private — not a Looker entry point". When adding a helper, give it a trailing underscore unless Looker is meant to call it.

Request flow (one Looker chart refresh)

Looker Studio                          Plausible CE
──────────────                         ─────────────
getData(request)
  └─ Code.gs::getData
      ├─ loadCredentials_     (Util.gs:  UserProperties → {apiUrl, apiKey})
      ├─ buildFields_         (Schema.gs: STATIC_FIELDS + dynamic prop_* dims)
      ├─ buildQuery_          (Query.gs)
      │     └─ translateFilters_  (Filters.gs: dimensionsFilters → Plausible DSL)
      ├─ plausibleQueryAll_   (Api.gs:   POST /api/v2/query, paginated) ──► Plausible
      └─ mapResponseToRows_   (Query.gs: Plausible results → Looker rows)

Field model

Schema.gs::STATIC_FIELDS is the single source of truth for what's exposed to Looker. Each field carries:

  • id — the Looker field ID (e.g. bounce_rate, prop_plan)
  • plausible — the API-side dimension or metric name (e.g. bounce_rate, event:props:plan)
  • kind'metric' or 'dimension'
  • looker.type — Looker FieldType string, resolved at runtime via cc.FieldType[...]
  • optional scale — multiplier applied in convertMetricValue_

Custom properties from configParams.customProps get appended dynamically by buildFields_ as prop_<name> fields mapped to event:props:<name>. Both schema generation and filter translation must go through buildFields_ / buildFieldToPlausibleMap_ — never hardcode field-id ↔ Plausible-name pairs anywhere else.

Non-obvious behaviors to preserve

  • Percent scaling: Plausible returns bounce_rate, scroll_depth, conversion_rate, group_conversion_rate, percentage as 0–100. Looker's PERCENT type expects 0–1. The scale: 0.01 field property + convertMetricValue_ handle this. If you add a new percent metric, set scale or values will display 100× too large.
  • Goal-filter heuristic (Filters.gs::translateOne_): a filter on event:goal is wrapped in has_done/has_not_done only when goal is not also a requested dimension. Without this, "visitors who completed Goal X grouped by source" returns goal-event counts, not session counts. Don't simplify away this branch.
  • Auth probe tolerates 404/400 (Auth.gs::validateCredentials_): the probe runs before a Site ID exists, so an unknown-site response is treated as auth-success. Only 401/403 and network errors fail the credential check.
  • forFilterOnly field handling (Code.gs::getData): When Looker Studio limits chart slices/segments to N < actual data rows (pie chart, donut, treemap, etc.), it marks the grouping dimension as forFilterOnly: true in request.fields AND adds a paired IS_NULL filter condition for that field in request.dimensionsFilters. The connector must keep such dimensions in the Plausible query and response schema — Looker still expects grouped data. To distinguish these "Others-grouping" dimensions from true filter-only fields (e.g. goal used as a filter condition, which must be excluded from the query), the connector scans request.dimensionsFilters for IS_NULL operators: any forFilterOnly field whose name appears in an IS_NULL entry is treated as a grouping dimension and included; all other forFilterOnly fields are excluded as before. Don't collapse this logic — the two cases look identical except for the IS_NULL signal.
  • Filter "all or none": when any Looker filter can't be translated (numeric ops on metrics, IS_NULL), translateFilters_ skips that filter and the response is returned with filtersApplied: false — Looker then filters client-side. Don't change this to a hard error.
  • Pagination cap: MAX_PAGES = 10 × PAGE_SIZE = 10000 = 100k rows. Apps Script has a 6-minute execution limit; raising the cap risks timeouts.
  • urlFetchWhitelist is intentionally absent from appsscript.json because the Plausible URL is user-supplied at auth time. This means the connector can only be deployed privately/per-workspace, not to the Looker Marketplace — that's a design constraint, not an oversight.
  • Date format conversion is per-granularity (Util.gs::formatLookerDate_). time:week requires ISO-week math via isoWeekFromDate_ — Plausible returns the week's Monday date, Looker expects YYYYWW.

Error surface

Api.gs defines a PlausibleError class with kind-tagged variants (AUTH, NOT_FOUND, RATE_LIMIT, NETWORK, HTTP, PARSE). Code.gs::handlePlausibleError_ is the single place that translates these into German user-facing messages via cc.newUserError().setText(...).throwException(). Add new error kinds in both places.