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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
20 changes: 4 additions & 16 deletions .agents/features/analytics.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
# Analytics

## Summary
The Analytics module provides platform-level reporting on automation usage: daily run counts, active flow counts, active user counts, and time-saved estimates. It powers an "Impact" dashboard for project-level drill-down and a "Leaderboard" view that ranks projects and users by automation output. Reports are cached with a 5-minute TTL and refreshed via a distributed-lock background job; a separate daily cron tracks per-piece usage across all flows.
The Analytics module provides platform-level reporting on automation usage: daily run counts, active flow counts, active user counts, and time-saved estimates. It powers an "Impact" dashboard for project-level drill-down. Reports are cached with a 5-minute TTL and refreshed via a distributed-lock background job; a separate daily cron tracks per-piece usage across all flows.

## Key Files
- `packages/server/api/src/app/analytics/` — backend module (controller, two services, entity)
- `packages/core/shared/src/lib/management/analytics/index.ts` — all shared Zod schemas and enums (`AnalyticsTimePeriod`, `PlatformAnalyticsReport`, `ProjectLeaderboardItem`, `UserLeaderboardItem`, etc.)
- `packages/core/shared/src/lib/management/analytics/index.ts` — all shared Zod schemas and enums (`AnalyticsTimePeriod`, `PlatformAnalyticsReport`, `AnalyticsReportRequest`, etc.)
- `packages/web/src/features/platform-admin/api/analytics-api.ts` — frontend API client
- `packages/web/src/features/platform-admin/hooks/analytics-hooks.ts` — TanStack Query hooks (`platformAnalyticsHooks`)
- `packages/web/src/app/routes/impact/index.tsx` — Impact page root
- `packages/web/src/app/routes/impact/summary/index.tsx` — summary metrics (active flows, users, runs, time saved)
- `packages/web/src/app/routes/impact/trends/index.tsx` — time-series area charts
- `packages/web/src/app/routes/impact/details/index.tsx` — per-flow drill-down with editable time-saved
- `packages/web/src/app/routes/leaderboard/index.tsx` — leaderboard page root
- `packages/web/src/app/routes/leaderboard/projects-leaderboard.tsx` — projects leaderboard table
- `packages/web/src/app/routes/leaderboard/users-leaderboard.tsx` — users leaderboard table

## Edition Availability
- **Community (CE)**: Not available — gated behind `analyticsEnabled` plan flag.
Expand All @@ -28,7 +25,7 @@ The Analytics module provides platform-level reporting on automation usage: dail
- **PlatformAnalyticsReport**: Cached entity holding daily run aggregations, enabled-flow metadata, and user list for a platform.
- **AnalyticsTimePeriod**: Enum for time windows (`LAST_WEEK`, `LAST_MONTH`, `LAST_THREE_MONTHS`, `LAST_SIX_MONTHS`, `LAST_YEAR`).
- **timeSavedPerRun**: Per-flow estimate (in minutes) of manual time saved per automation run; editable by the flow owner.
- **minutesSaved**: Derived metric = `runs × timeSavedPerRun`; displayed on leaderboards and impact summary.
- **minutesSaved**: Derived metric = `runs × timeSavedPerRun`; displayed on the impact summary.
- **outdated**: Boolean flag on the report entity indicating a background refresh is needed.
- **Pieces analytics**: Separate service that counts how many projects actively use each piece and updates `pieceMetadata.usage`.

Expand All @@ -53,8 +50,6 @@ Tracks which pieces are actively used:
**Key methods**:
- `refreshReport(platformId)` — distributed lock (400s), queries users + enabled flows + daily run counts (PRODUCTION only), merges incrementally. Stored as entity.
- `getOrGenerateReport(platformId, timePeriod?)` — returns cached report (5-min TTL), filters by time period
- `getProjectLeaderboard(platformId, timePeriod)` — groups flows by project, calculates minutes saved
- `getUserLeaderboard(platformId, timePeriod)` — groups flows by owner, calculates minutes saved
- `markAsOutdated(platformId)` — flags report for refresh

## Time Periods
Expand All @@ -63,17 +58,10 @@ Tracks which pieces are actively used:

Minutes saved = runs count × flow.timeSavedPerRun

## Leaderboard

**Users**: rank, userName, email, flowCount, minutesSaved, badges[]
**Projects**: rank, projectName, flowCount, minutesSaved

Displayed at `/leaderboard` in frontend with time period selector + search + time-saved range filter.

## Gating

`analyticsEnabled` plan flag. Module uses `platformMustHaveFeatureEnabled((p) => p.plan.analyticsEnabled)`.

## Frontend

All analytics queries in `platformAnalyticsHooks` include `enabled: platform.plan.analyticsEnabled` to prevent firing when the feature is off. The Impact page (`/impact`) is split into Summary, Trends, and Details sub-routes. The Leaderboard page (`/leaderboard`) shows separate tabs for projects and users, each with a time period picker. A "Refresh" button triggers `useRefreshAnalytics` mutation which calls the backend refresh endpoint and invalidates all analytics query keys.
All analytics queries in `platformAnalyticsHooks` include `enabled: platform.plan.analyticsEnabled` to prevent firing when the feature is off. The Impact page (`/impact`) is split into Summary, Trends, and Details sub-routes. A "Refresh" button triggers `useRefreshAnalytics` mutation which calls the backend refresh endpoint and invalidates all analytics query keys.
4 changes: 2 additions & 2 deletions .agents/features/flows.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ The step-settings sidebar hosts a step data panel with two layouts, switched via

Two builder surfaces consume an action/trigger's optional `outputSchema` (defined on the piece — see [pieces.md](./pieces.md)):

- **Smart Output Viewer** (`components/custom/smart-output-viewer/`) — used by the step data panel's output pane and run details. With hints, renders a labelled friendly view driven by the hints' `fields` array (type icons, copy-to-clipboard, expandable nested values via `children` / `listItems`, automatic table view for arrays of records, formatted images / emails / dates / file sizes / durations / currencies). Without hints, falls back to a generic field list for arbitrary JSON. A Raw JSON tab is always available.
- **Data Selector** (`app/builder/data-selector/`) — variable picker. With hints, the Friendly tab shows labelled rows with value previews (purple values, same formatting as the viewer); inserting a row produces a fully-qualified mention path wrapped in the step's `['output']` accessor (e.g. `step_1['output']['thread']['data']['messages'][0]['subject']`). Every friendly-view path is built through the single `pathHelpers.propertyPathStarter(stepName)` helper (`path-helpers.ts`), which prepends `['output']` — so array items, primitive outputs, and schema fields all resolve at runtime (the Advanced tab does the same via `buildJsonPath`). Each step root (array / primitive / schema object) is itself insertable as `step['output']`. The root piece icon is resolved from the node's `stepName` field, **not** its `propertyPath` (which contains `['output']` and would fail `getStep`'s exact name match). Without hints, falls back to a generic per-step field list. The Advanced tab is the existing raw tree.
- **Smart Output Viewer** (`components/custom/smart-output-viewer/`) — used by the step data panel's output pane and run details. With hints, renders a labelled friendly view driven by the hints' `fields` array (type icons, copy-to-clipboard, expandable nested values via `children` / `listItems`, automatic table view for arrays of records, formatted images / emails / dates / file sizes / durations / currencies). Without hints, falls back to a generic field list for arbitrary JSON; object keys are rendered verbatim. A Raw JSON tab is always available.
- **Data Selector** (`app/builder/data-selector/`) — variable picker. With hints, the Friendly tab shows labelled rows with value previews (purple values, same formatting as the viewer); inserting a row produces a fully-qualified mention path wrapped in the step's `['output']` accessor (e.g. `step_1['output']['thread']['data']['messages'][0]['subject']`). Every friendly-view path is built through the single `pathHelpers.propertyPathStarter(stepName)` helper (`path-helpers.ts`), which prepends `['output']` — so array items, primitive outputs, and schema fields all resolve at runtime (the Advanced tab does the same via `buildJsonPath`). Each step root (array / primitive / schema object) is itself insertable as `step['output']`. The root piece icon is resolved from the node's `stepName` field, **not** its `propertyPath` (which contains `['output']` and would fail `getStep`'s exact name match). Without hints, falls back to a generic per-step field list; object keys are rendered verbatim. The Advanced tab is the existing raw tree.

Both surfaces fetch hints via `usePieceOutputSchema({ pieceName, pieceVersion, stepName })`, which reads from the cached `['piece', name, version]` React Query entry — no extra network calls. Hint path lookups use `pathUtils.getValueByDotPath` (`packages/web/src/lib/path-utils.ts`), which supports dot/bracket notation and a wrapper-key fallback (`data.*`, `body.*`, `payload.*`, …) so common API envelopes resolve transparently.

Expand Down
6 changes: 3 additions & 3 deletions .agents/features/workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Workers are separate Node processes that poll the app for jobs and execute flows
- `packages/server/api/src/app/workers/machine/machine-service.ts` — `onConnection` / `onDisconnect`, `buildSettingsResponse` (emits `APP_VERSION`), worker listing
- `packages/server/api/src/app/workers/rpc/worker-rpc-service.ts` — `createHandlers()`: `poll` (with version gate), `completeJob`, `extendLock`, progress/log RPCs, `getFlowBundle`, `prepareFlowBundleUpload`, `uploadFlowBundle`
- `packages/server/worker/src/lib/worker.ts` — worker lifecycle (`worker.start/stop`), `pollAndExecute` loop (with version gate, tracks `inFlightJobs`), `getWorkerProps`; builds the `Runtime` via `createSandboxRuntime` and a per-job `Resolver` via `createResolver`. `stop()` drains in-flight jobs (`drainInFlightJobs`) before teardown; an unexpected disconnect aborts the runtime (`abortInFlightRuntime`)
- `packages/server/worker/src/lib/runtime/sandbox-config.ts` — bridges worker settings → `SandboxSettings` + cache base path (merges the env-only `AP_REUSE_SANDBOX` override into the fetched `WorkerSettings`)
- `packages/server/worker/src/lib/runtime/sandbox-config.ts` — bridges worker settings → `SandboxSettings` + cache base path (merges the env-only `AP_REUSE_SANDBOX` override into the fetched `WorkerSettings`); when `concurrency === 1`, `primeFullContainerMemory()` (called on each startup/reconnect) reads the container's total RAM (cgroup-aware) and overrides `SANDBOX_MEMORY_LIMIT` so the single sandbox receives the full container memory (best-effort — the server-configured limit stays if the read fails)
- `packages/server/sandbox/` — standalone `@activepieces/sandbox` library holding the in-process sandbox (`createSandboxRuntime`, `sandbox-manager`, isolate/fork process makers), cache/piece installation, the worker-side `createResolver`, and the `Runtime` / `Resolver` / `ProvisionInput` type contracts (`src/lib/types.ts`)
- `packages/server/worker/src/lib/config/configs.ts` — worker env vars incl. `AP_REUSE_SANDBOX` (`WorkerSystemProp.REUSE_SANDBOX`, reuse the engine process between jobs), `AP_WORKER_CONCURRENCY`, `AP_WORKER_GROUP_ID`, and `AP_PROJECT_WORKER` (`WorkerSystemProp.PROJECT_WORKER`, boolean, default `true` — flags the group id as project-scoped; set `false` for a platform group)
- `packages/server/worker/src/lib/config/worker-settings.ts` — caches the `WorkerSettingsResponse` fetched on connect
Expand All @@ -25,7 +25,7 @@ Workers are separate Node processes that poll the app for jobs and execute flows
> Canonical term definitions live in the bounded-context glossaries — see [CONTEXT-MAP.md](../../CONTEXT-MAP.md).

- **`Resolver` / `Runtime`** — the two roles of the in-process `@activepieces/sandbox` library that the worker drives per job. The **`Resolver`** (worker-side, owns the only `apiClient`) turns a job into a fully-materialized `ProvisionInput`: it resolves the `flowVersion`, piece metadata, and a ready (compiled) flow bundle, disabling the flow on a missing piece. It returns a discriminated `ResolveResult` (`ready` — with the resolved `flowVersion` when a `flow` was passed; `flow-not-found`; `disabled`) rather than throwing for expected missing/disabled-flow cases. The **`Runtime`** (the in-process single sandbox box) exposes `execute` / `getActiveExecutors` / `shutdown` and never reaches the app; `execute` owns the box lifecycle internally — **acquire → provision (materialize pieces/code into the bind-mounted cache) → run one engine operation → release on success / invalidate on throw** — re-raising the sandbox `ActivepiecesError` codes (timeout / memory / log-size) that handlers already catch. A code step whose **dependency install (`bun install`) or compile (`esbuild`) phase fails** is degraded into a valid runtime-throwing `index.js` stub (see `code-builder.ts`) rather than letting the error escape provision — so the run ends with a user-attributed **`FAILED`** step instead of an opaque **`INTERNAL_ERROR`** that would trigger retries (the inputs are user data, not a platform fault). The Resolver's bundle path can return a **bundle hit** from the flow bundle store (`getFlowBundle` RPC): when a locked flow version's bundle is already cached, it supplies the frozen flow definition + piece manifest + compiled code directly, bypassing the per-piece `getPiece` RPC round-trips. Locked flows that change pieces must be re-locked to produce a new bundle; a freshly resolved locked version is published back (best-effort, after execution cache setup). **Bundle transport:** when the `FLOW_BUNDLE` file is S3-backed and `S3_USE_SIGNED_URLS` is on, the bundle bytes never cross the Socket.IO RPC — `getFlowBundle` returns a signed GET URL and `prepareFlowBundleUpload` returns a signed PUT URL, and the worker reads/writes S3 directly via the built-in `fetch` (`sandbox/src/lib/utils/bundle-http.ts`, dependency-free). S3-backed bundles without signed URLs stream inline `Buffer` over the socket (`uploadFlowBundle`). On **DB-backed storage bundles are not persisted at all** — `prepareFlowBundleUpload` returns `{ kind: 'skip' }` and the worker always builds inline via the legacy resolve path (DB storage gains nothing from bundle caching and a null-data pre-save would throw). Any signed-link or fetch failure likewise degrades to the legacy `getVersion` + `getPiece` resolve path — a bundle never fails a run.
- **`WorkerProps`** — typed worker identity sent in every heartbeat (`EXECUTION_MODE`, `WORKER_CONCURRENCY`, `SANDBOX_MEMORY_LIMIT`, `REUSE_SANDBOX`, `version`). Previously a free-form `Record<string,string>`.
- **`WorkerProps`** — typed worker identity sent in every heartbeat (`EXECUTION_MODE`, `WORKER_CONCURRENCY`, `SANDBOX_MEMORY_LIMIT`, `REUSE_SANDBOX`, `version`). Previously a free-form `Record<string,string>`. `SANDBOX_MEMORY_LIMIT` reports the **effective** limit (from `sandboxConfig.getSandboxSettings()`), i.e. the full container RAM after the concurrency-1 prime, not the raw server-configured value.
- **`WorkerSettingsResponse`** — runtime config the app hands a worker on connect; now includes `APP_VERSION` (the app's release).
- **`connectionGeneration`** — worker-side counter bumped on every disconnect; in-flight poll loops exit when their captured generation goes stale, so a reconnect starts fresh loops.
- **version gate** — both sides refuse to exchange jobs when worker release ≠ app release (see below).
Expand All @@ -36,7 +36,7 @@ Workers are separate Node processes that poll the app for jobs and execute flows

## Connection & Poll Flow
1. Worker connects → emits `FETCH_WORKER_SETTINGS`; app's `machineService.onConnection` returns `WorkerSettingsResponse` (incl. `APP_VERSION`) and registers `createHandlers` for the socket.
2. Worker caches settings, **shuts down any old `Runtime` (fire-and-forget) and creates a fresh one**, then spawns `concurrency` `pollAndExecute` loops. Recreating per (re)connect means an in-flight job from a prior connection is killed with its box (it fails fast and BullMQ retries it) rather than lingering on a reused box — reusing a busy box let the new generation collide on the single-operation engine child and let the lingering job's lock lapse → BullMQ `Job stalled`.
2. Worker caches settings, **shuts down any old `Runtime` (fire-and-forget) and creates a fresh one**, then spawns `concurrency` `pollAndExecute` loops. When `concurrency === 1`, it calls `sandboxConfig.primeFullContainerMemory()` before building the runtime, so the single sandbox is sized to the container ceiling rather than the server-configured limit. Recreating per (re)connect means an in-flight job from a prior connection is killed with its box (it fails fast and BullMQ retries it) rather than lingering on a reused box — reusing a busy box let the new generation collide on the single-operation engine child and let the lingering job's lock lapse → BullMQ `Job stalled`.
3. Each loop calls `apiClient.poll(machineInfo)`; the app's `poll` handler returns the next job for the worker's queue, or `null`. Handing out a job moves it to BullMQ `active`, so the handler **records the job under the polling connection's `socket.id`** (`jobAssignmentTracker`, keyed by queue+jobId) — only the app knows which connection holds it. (Scoped to `socket.id`, not the stable `workerId`, so a late disconnect of an old socket can't reclaim jobs a reconnected socket already polled.)
4. On job: worker executes in a sandbox, periodically `extendLock`, then `completeJob`; `completeJob` **clears the assignment**.
5. On disconnect, `connectionGeneration++` stops the loops; the app **returns that connection's still-held jobs to the queue** (`jobBroker.releaseConnectionJobs(socket.id)`, called from the `DISCONNECT` handler — scoped to the socket so a late disconnect can't reclaim a reconnected socket's jobs) so they don't sit orphaned in `active` past worker concurrency (the deploy "Job stalled" storm) waiting on the minutes-long stalled-scan; the worker **aborts its in-flight runtime** (`abortInFlightRuntime`) so it stops executing a job the app is reclaiming. Socket.IO auto-reconnects (incl. a manual reconnect on `io server disconnect`, e.g. an app restart during a deploy) and the cycle repeats — recreating the runtime as in step 2.
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/smoke-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ jobs:
# can't exercise isolation here (it returns no data and the gate fails spuriously). Run it
# locally with `benchmark/probe-fs.js` + `smoke-test/verify-isolation.sh` (both kept in repo).

# Memory-limit detection gate (same SANDBOX_CODE_ONLY stack): the OOM input expression blows
# the ENGINE heap via the isolate->engine result copy, and AP_WORKER_CONCURRENCY=1 (job env)
# gives that single sandbox the full 1G worker container. Every run must end
# MEMORY_LIMIT_EXCEEDED — this pins the detection path for production code-only engine OOMs.
- name: Setup OOM flow
id: setup-oom
run: |
FLOW_ID=$(CODE_INPUT_SUM="$(cat benchmark/oom-expression.txt)" benchmark/setup.sh)
echo "flow_id=$FLOW_ID" >> "$GITHUB_OUTPUT"
echo "Flow ID: $FLOW_ID"

- name: Run memory-limit smoke test
run: |
chmod +x smoke-test/verify-memory-limit.sh
smoke-test/verify-memory-limit.sh "${{ steps.setup-oom.outputs.flow_id }}"

- name: Restart stack in S3 mode (MinIO, no signed URLs)
run: |
docker compose -f benchmark/docker-compose.yml -f benchmark/docker-compose.minio.yml down -v
Expand Down
Loading
Loading