From 4f9088559f95ee0e8e1596bdef5a95abcc5e9733 Mon Sep 17 00:00:00 2001 From: Dhruwang Date: Wed, 18 Feb 2026 18:05:47 +0530 Subject: [PATCH 1/2] feat: add Cube.js dev setup and analytics client - Add Cube container to docker-compose.dev.yml (pinned v1.3.21) - Add Cube server config (cube/cube.js) and FeedbackRecords schema - Add @cubejs-client/core dependency and singleton client in EE module - Add CUBEJS_API_URL and CUBEJS_API_TOKEN to .env.example Co-authored-by: Cursor --- .env.example | 6 + .../ee/analytics/api/lib/cube-client.ts | 25 +++ apps/web/package.json | 1 + cube/cube.js | 12 ++ cube/schema/FeedbackRecords.js | 159 ++++++++++++++++++ docker-compose.dev.yml | 25 +++ pnpm-lock.yaml | 73 ++++++-- 7 files changed, 284 insertions(+), 17 deletions(-) create mode 100644 apps/web/modules/ee/analytics/api/lib/cube-client.ts create mode 100644 cube/cube.js create mode 100644 cube/schema/FeedbackRecords.js diff --git a/.env.example b/.env.example index 78eac054f7..ceccb2ce09 100644 --- a/.env.example +++ b/.env.example @@ -229,5 +229,11 @@ REDIS_URL=redis://localhost:6379 # AUDIT_LOG_GET_USER_IP=0 +# Cube.js Analytics (optional — only needed for the analytics/dashboard feature) +# URL where the Cube.js instance is running +# CUBEJS_API_URL=http://localhost:4000 +# API token sent with each Cube.js request (empty is accepted when CUBEJS_DEV_MODE=true) +# CUBEJS_API_TOKEN= + # Lingo.dev API key for translation generation LINGODOTDEV_API_KEY=your_api_key_here \ No newline at end of file diff --git a/apps/web/modules/ee/analytics/api/lib/cube-client.ts b/apps/web/modules/ee/analytics/api/lib/cube-client.ts new file mode 100644 index 0000000000..fb3babfdc4 --- /dev/null +++ b/apps/web/modules/ee/analytics/api/lib/cube-client.ts @@ -0,0 +1,25 @@ +import cubejs, { type CubeApi, type Query } from "@cubejs-client/core"; + +const getApiUrl = (): string => { + const baseUrl = process.env.CUBEJS_API_URL || "http://localhost:4000"; + if (baseUrl.includes("/cubejs-api/v1")) { + return baseUrl; + } + return `${baseUrl.replace(/\/$/, "")}/cubejs-api/v1`; +}; + +let cubeClient: CubeApi | null = null; + +function getCubeClient(): CubeApi { + if (!cubeClient) { + const token = process.env.CUBEJS_API_TOKEN ?? ""; + cubeClient = cubejs(token, { apiUrl: getApiUrl() }); + } + return cubeClient; +} + +export async function executeQuery(query: Query) { + const client = getCubeClient(); + const resultSet = await client.load(query); + return resultSet.tablePivot(); +} diff --git a/apps/web/package.json b/apps/web/package.json index 727588fe7c..a65b1d2121 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,6 +23,7 @@ "@aws-sdk/s3-presigned-post": "3.971.0", "@aws-sdk/s3-request-presigner": "3.971.0", "@boxyhq/saml-jackson": "1.52.2", + "@cubejs-client/core": "1.6.6", "@dnd-kit/core": "6.3.1", "@dnd-kit/modifiers": "9.0.0", "@dnd-kit/sortable": "10.0.0", diff --git a/cube/cube.js b/cube/cube.js new file mode 100644 index 0000000000..8e38146bc0 --- /dev/null +++ b/cube/cube.js @@ -0,0 +1,12 @@ +module.exports = { + // queryRewrite runs before every Cube query. Use it to enforce row-level security (RLS) + // by injecting filters based on the caller's identity (e.g. organizationId, projectId). + // + // The securityContext is populated from the decoded JWT passed via the API token. + // Currently a passthrough because access control is handled in the Next.js API layer + // before reaching Cube. When Cube is exposed more broadly or multi-tenancy enforcement + // is needed at the Cube level, add filters here based on securityContext claims. + queryRewrite: (query, { securityContext }) => { + return query; + }, +}; diff --git a/cube/schema/FeedbackRecords.js b/cube/schema/FeedbackRecords.js new file mode 100644 index 0000000000..6742933303 --- /dev/null +++ b/cube/schema/FeedbackRecords.js @@ -0,0 +1,159 @@ +// This schema maps to the `feedback_records` table owned by the Formbricks Hub Postgres. +// If the Hub changes column names, types, or the metadata JSONB shape (e.g. the `topics` array), +// this schema must be updated to match. +cube(`FeedbackRecords`, { + sql: `SELECT * FROM feedback_records`, + + measures: { + count: { + type: `count`, + description: `Total number of feedback responses`, + }, + + promoterCount: { + type: `count`, + filters: [{ sql: `${CUBE}.value_number >= 9` }], + description: `Number of promoters (NPS score 9-10)`, + }, + + detractorCount: { + type: `count`, + filters: [{ sql: `${CUBE}.value_number <= 6` }], + description: `Number of detractors (NPS score 0-6)`, + }, + + passiveCount: { + type: `count`, + filters: [{ sql: `${CUBE}.value_number >= 7 AND ${CUBE}.value_number <= 8` }], + description: `Number of passives (NPS score 7-8)`, + }, + + npsScore: { + type: `number`, + sql: ` + CASE + WHEN COUNT(*) = 0 THEN 0 + ELSE ROUND( + ( + (COUNT(CASE WHEN ${CUBE}.value_number >= 9 THEN 1 END)::numeric - + COUNT(CASE WHEN ${CUBE}.value_number <= 6 THEN 1 END)::numeric) + / COUNT(*)::numeric + ) * 100, + 2 + ) + END + `, + description: `Net Promoter Score: ((Promoters - Detractors) / Total) * 100`, + }, + + averageScore: { + type: `avg`, + sql: `${CUBE}.value_number`, + description: `Average NPS score`, + }, + }, + + dimensions: { + id: { + sql: `id`, + type: `string`, + primaryKey: true, + }, + + sentiment: { + sql: `sentiment`, + type: `string`, + description: `Sentiment extracted from metadata JSONB field`, + }, + + sourceType: { + sql: `source_type`, + type: `string`, + description: `Source type of the feedback (e.g., nps_campaign, survey)`, + }, + + sourceName: { + sql: `source_name`, + type: `string`, + description: `Human-readable name of the source`, + }, + + fieldType: { + sql: `field_type`, + type: `string`, + description: `Type of feedback field (e.g., nps, text, rating)`, + }, + + collectedAt: { + sql: `collected_at`, + type: `time`, + description: `Timestamp when the feedback was collected`, + }, + + npsValue: { + sql: `value_number`, + type: `number`, + description: `Raw NPS score value (0-10)`, + }, + + responseId: { + sql: `response_id`, + type: `string`, + description: `Unique identifier linking related feedback records`, + }, + + userIdentifier: { + sql: `user_identifier`, + type: `string`, + description: `Identifier of the user who provided feedback`, + }, + + emotion: { + sql: `emotion`, + type: `string`, + description: `Emotion extracted from metadata JSONB field`, + }, + }, + + joins: { + TopicsUnnested: { + sql: `${CUBE}.id = ${TopicsUnnested}.feedback_record_id`, + relationship: `hasMany`, + }, + }, +}); + +cube(`TopicsUnnested`, { + sql: ` + SELECT + fr.id as feedback_record_id, + topic_elem.topic + FROM feedback_records fr + CROSS JOIN LATERAL jsonb_array_elements_text(COALESCE(fr.metadata->'topics', '[]'::jsonb)) AS topic_elem(topic) + `, + + measures: { + count: { + type: `count`, + }, + }, + + dimensions: { + id: { + sql: `feedback_record_id || '-' || topic`, + type: `string`, + primaryKey: true, + }, + + feedbackRecordId: { + sql: `feedback_record_id`, + type: `string`, + }, + + topic: { + sql: `topic`, + type: `string`, + description: `Individual topic from the topics array`, + }, + }, +}); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index cb454d512e..0ec6c068eb 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -36,6 +36,31 @@ services: volumes: - minio-data:/data + # Cube connects to the Formbricks Hub Postgres which owns the feedback_records table. + # The CUBEJS_DB_* defaults below must match the Hub's Postgres credentials. + # Override via env vars if your Hub database uses different credentials or runs on another host. + cube: + image: cubejs/cube:v1.3.21 + ports: + - 4000:4000 + - 4001:4001 # Cube Playground UI (dev only) + environment: + CUBEJS_DB_TYPE: postgres + CUBEJS_DB_HOST: ${CUBEJS_DB_HOST:-postgres} + CUBEJS_DB_NAME: ${CUBEJS_DB_NAME:-postgres} + CUBEJS_DB_USER: ${CUBEJS_DB_USER:-postgres} + CUBEJS_DB_PASS: ${CUBEJS_DB_PASS:-postgres} + CUBEJS_DB_PORT: ${CUBEJS_DB_PORT:-5432} + CUBEJS_DEV_MODE: "true" + CUBEJS_API_SECRET: ${CUBEJS_API_SECRET:-changeme} + CUBEJS_CACHE_AND_QUEUE_DRIVER: memory + volumes: + - ./cube/cube.js:/cube/conf/cube.js + - ./cube/schema:/cube/conf/model + restart: on-failure + depends_on: + - postgres + volumes: postgres: driver: local diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f337c9df6..70d260d9e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: dependencies: next: specifier: 16.1.6 - version: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: specifier: 19.2.3 version: 19.2.3 @@ -139,6 +139,9 @@ importers: '@boxyhq/saml-jackson': specifier: 1.52.2 version: 1.52.2(socks@2.8.7)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.8.3)) + '@cubejs-client/core': + specifier: 1.6.6 + version: 1.6.6(encoding@0.1.13) '@dnd-kit/core': specifier: 6.3.1 version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -294,7 +297,7 @@ importers: version: 1.2.6(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@sentry/nextjs': specifier: 10.5.0 - version: 10.5.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.12)) + version: 10.5.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.12)) '@t3-oss/env-nextjs': specifier: 0.13.4 version: 0.13.4(arktype@2.1.29)(typescript@5.8.3)(zod@3.24.4) @@ -384,13 +387,13 @@ importers: version: 3.0.1 next: specifier: 16.1.6 - version: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-auth: specifier: 4.24.12 - version: 4.24.12(patch_hash=7ac5717a8d7d2049442182b5d83ab492d33fe774ff51ff5ea3884628b77df87b)(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 4.24.12(patch_hash=7ac5717a8d7d2049442182b5d83ab492d33fe774ff51ff5ea3884628b77df87b)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-safe-action: specifier: 7.10.8 - version: 7.10.8(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.24.4) + version: 7.10.8(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.24.4) node-fetch: specifier: 3.3.2 version: 3.3.2 @@ -1775,6 +1778,9 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@cubejs-client/core@1.6.6': + resolution: {integrity: sha512-JWEVQaaS7y6Y3Nhe4Lcjl8coP5eXIgYZQTb2WMlLdMluSGM4mWOSFpAis77lAPV+nybFvRf9ri+PjCGqGJXH5g==} + '@date-fns/tz@1.2.0': resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==} @@ -7155,6 +7161,9 @@ packages: core-js-compat@3.47.0: resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==} + core-js@3.48.0: + resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -7170,6 +7179,9 @@ packages: engines: {node: '>=20'} hasBin: true + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -10035,6 +10047,9 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + ramda@0.27.2: + resolution: {integrity: sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -11306,6 +11321,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-search-params-polyfill@7.0.1: + resolution: {integrity: sha512-bAw7L2E+jn9XHG5P9zrPnHdO0yJub4U+yXJOdpcpkr7OBd9T8oll4lUos0iSGRcDvfZoLUKfx9a6aNmIhJ4+mQ==} + url-template@2.0.8: resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} @@ -13515,6 +13533,17 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@cubejs-client/core@1.6.6(encoding@0.1.13)': + dependencies: + core-js: 3.48.0 + cross-fetch: 3.2.0(encoding@0.1.13) + dayjs: 1.11.19 + ramda: 0.27.2 + url-search-params-polyfill: 7.0.1 + uuid: 11.1.0 + transitivePeerDependencies: + - encoding + '@date-fns/tz@1.2.0': {} '@dnd-kit/accessibility@3.1.1(react@19.2.3)': @@ -17049,7 +17078,7 @@ snapshots: '@sentry/core@10.5.0': {} - '@sentry/nextjs@10.5.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.12))': + '@sentry/nextjs@10.5.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.12))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.38.0 @@ -17062,7 +17091,7 @@ snapshots: '@sentry/vercel-edge': 10.5.0 '@sentry/webpack-plugin': 4.6.1(encoding@0.1.13)(webpack@5.99.8(esbuild@0.25.12)) chalk: 3.0.0 - next: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) resolve: 1.22.8 rollup: 4.54.0 stacktrace-parser: 0.1.11 @@ -19786,6 +19815,8 @@ snapshots: dependencies: browserslist: 4.28.1 + core-js@3.48.0: {} + core-util-is@1.0.3: {} cors@2.8.5: @@ -19800,6 +19831,12 @@ snapshots: '@epic-web/invariant': 1.0.0 cross-spawn: 7.0.6 + cross-fetch@3.2.0(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -22186,13 +22223,13 @@ snapshots: neo-async@2.6.2: {} - next-auth@4.24.12(patch_hash=7ac5717a8d7d2049442182b5d83ab492d33fe774ff51ff5ea3884628b77df87b)(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next-auth@4.24.12(patch_hash=7ac5717a8d7d2049442182b5d83ab492d33fe774ff51ff5ea3884628b77df87b)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@babel/runtime': 7.28.4 '@panva/hkdf': 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.28.2 @@ -22203,9 +22240,9 @@ snapshots: optionalDependencies: nodemailer: 7.0.11 - next-safe-action@7.10.8(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.24.4): + next-safe-action@7.10.8(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.24.4): dependencies: - next: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: @@ -22219,7 +22256,7 @@ snapshots: postcss: 8.4.31 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3) + styled-jsx: 5.1.6(react@19.2.3) optionalDependencies: '@next/swc-darwin-arm64': 16.0.10 '@next/swc-darwin-x64': 16.0.10 @@ -22236,7 +22273,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@next/env': 16.1.6 '@swc/helpers': 0.5.15 @@ -22245,7 +22282,7 @@ snapshots: postcss: 8.4.31 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3) + styled-jsx: 5.1.6(react@19.2.3) optionalDependencies: '@next/swc-darwin-arm64': 16.1.6 '@next/swc-darwin-x64': 16.1.6 @@ -22933,6 +22970,8 @@ snapshots: quick-format-unescaped@4.0.4: {} + ramda@0.27.2: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -23991,12 +24030,10 @@ snapshots: stubborn-utils@1.0.2: {} - styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.3): + styled-jsx@5.1.6(react@19.2.3): dependencies: client-only: 0.0.1 react: 19.2.3 - optionalDependencies: - '@babel/core': 7.28.5 stylis@4.3.6: {} @@ -24504,6 +24541,8 @@ snapshots: dependencies: punycode: 2.3.1 + url-search-params-polyfill@7.0.1: {} + url-template@2.0.8: {} use-callback-ref@1.3.3(@types/react@19.2.1)(react@19.2.1): From 4dcf6fda403654f79fec66880551a02502f68a8b Mon Sep 17 00:00:00 2001 From: Dhruwang Date: Wed, 18 Feb 2026 18:44:24 +0530 Subject: [PATCH 2/2] fix: code rabbit feedback --- .env.example | 5 +++- .../ee/analytics/api/lib/cube-client.test.ts | 29 +++++++++++++++++++ docker-compose.dev.yml | 7 +++-- turbo.json | 2 ++ 4 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 apps/web/modules/ee/analytics/api/lib/cube-client.test.ts diff --git a/.env.example b/.env.example index ceccb2ce09..72b62b487c 100644 --- a/.env.example +++ b/.env.example @@ -230,9 +230,12 @@ REDIS_URL=redis://localhost:6379 # Cube.js Analytics (optional — only needed for the analytics/dashboard feature) +# Required when running the Cube service (docker-compose.dev.yml). Generate with: openssl rand -hex 32 +# Use the same value for CUBEJS_API_TOKEN so the client can authenticate. +# CUBEJS_API_SECRET= # URL where the Cube.js instance is running # CUBEJS_API_URL=http://localhost:4000 -# API token sent with each Cube.js request (empty is accepted when CUBEJS_DEV_MODE=true) +# API token sent with each Cube.js request; must match CUBEJS_API_SECRET when CUBEJS_DEV_MODE is off # CUBEJS_API_TOKEN= # Lingo.dev API key for translation generation diff --git a/apps/web/modules/ee/analytics/api/lib/cube-client.test.ts b/apps/web/modules/ee/analytics/api/lib/cube-client.test.ts new file mode 100644 index 0000000000..3d6113b705 --- /dev/null +++ b/apps/web/modules/ee/analytics/api/lib/cube-client.test.ts @@ -0,0 +1,29 @@ +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { executeQuery } from "./cube-client"; + +const mockLoad = vi.fn(); +const mockTablePivot = vi.fn(); + +vi.mock("@cubejs-client/core", () => ({ + default: vi.fn(() => ({ + load: mockLoad, + })), +})); + +describe("executeQuery", () => { + beforeEach(() => { + vi.clearAllMocks(); + const resultSet = { tablePivot: mockTablePivot }; + mockLoad.mockResolvedValue(resultSet); + mockTablePivot.mockReturnValue([{ id: "1", count: 42 }]); + }); + + test("loads query and returns tablePivot result", async () => { + const query = { measures: ["FeedbackRecords.count"] }; + const result = await executeQuery(query); + + expect(mockLoad).toHaveBeenCalledWith(query); + expect(mockTablePivot).toHaveBeenCalled(); + expect(result).toEqual([{ id: "1", count: 42 }]); + }); +}); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 0ec6c068eb..4df6b62044 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -39,8 +39,11 @@ services: # Cube connects to the Formbricks Hub Postgres which owns the feedback_records table. # The CUBEJS_DB_* defaults below must match the Hub's Postgres credentials. # Override via env vars if your Hub database uses different credentials or runs on another host. + # + # SECURITY: CUBEJS_API_SECRET has no default and must be set explicitly (e.g. in .env). + # Never use a weak secret in production/staging. Generate with: openssl rand -hex 32 cube: - image: cubejs/cube:v1.3.21 + image: cubejs/cube:v1.6.6 ports: - 4000:4000 - 4001:4001 # Cube Playground UI (dev only) @@ -52,7 +55,7 @@ services: CUBEJS_DB_PASS: ${CUBEJS_DB_PASS:-postgres} CUBEJS_DB_PORT: ${CUBEJS_DB_PORT:-5432} CUBEJS_DEV_MODE: "true" - CUBEJS_API_SECRET: ${CUBEJS_API_SECRET:-changeme} + CUBEJS_API_SECRET: ${CUBEJS_API_SECRET} CUBEJS_CACHE_AND_QUEUE_DRIVER: memory volumes: - ./cube/cube.js:/cube/conf/cube.js diff --git a/turbo.json b/turbo.json index 7c160c19b2..fa213cb5e1 100644 --- a/turbo.json +++ b/turbo.json @@ -156,6 +156,8 @@ "BREVO_API_KEY", "BREVO_LIST_ID", "CRON_SECRET", + "CUBEJS_API_TOKEN", + "CUBEJS_API_URL", "DATABASE_URL", "DEBUG", "E2E_TESTING",