Skip to content

feat: doc status & share status#14426

Merged
darkskygit merged 3 commits intocanaryfrom
darksky/doc-sharedlink-status
Feb 12, 2026
Merged

feat: doc status & share status#14426
darkskygit merged 3 commits intocanaryfrom
darksky/doc-sharedlink-status

Conversation

@darkskygit
Copy link
Member

@darkskygit darkskygit commented Feb 12, 2026

PR Dependency Tree

This tree was auto-generated by Charcoal

Summary by CodeRabbit

  • New Features
    • Admin dashboard: view workspace analytics (storage, sync activity, top shared links) with charts and configurable windows.
    • Document analytics tab: see total/unique/guest views and trends over selectable time windows.
    • Last-accessed members: view who last accessed a document, with pagination.
    • Shared links analytics: browse and paginate all shared links with view/unique/guest metrics and share URLs.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

Adds workspace and document analytics: new DB tables and Prisma models, a WorkspaceAnalytics model, GraphQL types and queries for admin/dashboard and doc analytics, backend recording of doc views and active-user flush, frontend admin and document analytics UIs, tests, and related routing/i18n updates.

Changes

Cohort / File(s) Summary
Database & Prisma
packages/backend/server/migrations/20260212053401_workspace_analytics/migration.sql, packages/backend/server/schema.prisma
Creates analytics tables and indexes (workspace_admin_stats_daily, sync_active_users_minutely, workspace_doc_view_daily, workspace_member_last_access); drops legacy features infrastructure; adds Prisma models/indexes for analytics.
Workspace Analytics Model
packages/backend/server/src/models/workspace-analytics.ts, packages/backend/server/src/models/index.ts
Adds WorkspaceAnalyticsModel with methods for admin dashboard, shared-links pagination, doc page analytics, last-access pagination, recording doc views, and minute-level active-user upserts; wired into models registry.
Backend Controllers & Recording
packages/backend/server/src/core/doc-renderer/controller.ts, packages/backend/server/src/core/workspaces/controller.ts, packages/backend/server/src/core/workspaces/stats.job.ts
Adds visitorId hashing helper, records shared and doc views via workspaceAnalytics.recordDocView, and writes daily admin snapshots after recalibration.
Sync Gateway & Presence
packages/backend/server/src/core/sync/gateway.ts, packages/backend/server/src/__tests__/sync/gateway.spec.ts
Implements socket presence extraction, per-minute active-user aggregation (with optional cluster aggregation), periodic flush timer, and tests/helpers for sync_active_users_minutely.
GraphQL Schema & Resolvers
packages/backend/server/src/schema.gql, packages/backend/server/src/core/workspaces/resolvers/admin.ts, packages/backend/server/src/core/workspaces/resolvers/doc.ts, packages/backend/server/src/core/workspaces/resolvers/analytics-types.ts, packages/common/graphql/src/schema.ts, packages/common/graphql/src/graphql/*.gql, packages/common/graphql/src/graphql/index.ts
Adds TimeWindow/TimeBucket, admin dashboard/shared-links types & queries, doc analytics and lastAccessedMembers types/queries, pagination guards and new GraphQL operations and generated schema types.
Pagination Validation
packages/backend/server/src/base/graphql/pagination.ts, packages/backend/server/src/base/graphql/__tests__/pagination.spec.ts
Introduces assertPaginationInput to forbid mixing after and offset, caps first and normalizes offset; adds corresponding test.
Backend Tests & E2E
packages/backend/server/src/__tests__/e2e/workspace/admin-analytics.spec.ts, packages/backend/server/src/__tests__/workspace/controller.spec.ts, packages/core/doc-renderer/__tests__/controller.spec.ts
Adds comprehensive e2e tests for admin analytics, pagination, permissions, and ensures doc view recording is invoked in controllers.
Frontend Admin Dashboard
packages/frontend/admin/src/modules/dashboard/index.tsx, packages/frontend/admin/src/components/ui/chart.tsx, packages/frontend/admin/src/app.tsx, packages/frontend/admin/src/modules/nav/nav.tsx, packages/frontend/admin/package.json
New Dashboard UI, chart primitives (ChartContainer, ChartTooltip), route integration, nav item, and recharts dependency.
Frontend Document Analytics
packages/frontend/core/src/desktop/pages/workspace/detail-page/.../tabs/analytics.tsx, .../analytics.css.ts, .../analytics.utils.ts, .../analytics.utils.spec.ts, detail-page.tsx
Adds EditorAnalyticsPanel, styles and utils for analytics windows, chart point helpers, viewer list, and tests; conditionally surfaced for cloud flavour.
Routing & Frontend Routes
packages/frontend/routes/routes.json, packages/frontend/routes/src/routes.ts
Adds admin dashboard route and path helpers.
I18n
packages/frontend/i18n/src/resources/en.json, packages/frontend/i18n/src/i18n.gen.ts, packages/frontend/i18n/src/i18n-completenesses.json
Adds analytics translation keys and updates completeness scores for locales.
Minor Refactors / Init Timing
packages/backend/server/src/plugins/payment/manager/common.ts, packages/backend/server/src/plugins/payment/service.ts, packages/backend/server/src/plugins/worker/service.ts
Move eager field initializations into constructors for ScheduleManager and allowedOrigins.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Server as AppServer
    participant Gateway as SyncGateway
    participant Cache as Redis
    participant DB as Postgres

    Client->>Server: HTTP GET /doc/:id (or shared page)
    Server->>Server: buildVisitorId(req, workspaceId, docId)
    Server->>Gateway: (optional) socket connect / presence
    Server->>DB: workspaceAnalytics.recordDocView(workspaceId, docId, visitorId, isGuest, userId?)
    DB->>DB: upsert workspace_doc_view_daily / workspace_member_last_access
    Server->>Cache: markDailyUniqueVisitor(docId, visitorId)
    Cache-->>DB: (unique visitor check/expiry)
    Client->>Server: GraphQL adminDashboard() / getDocPageAnalytics()
    Server->>DB: workspaceAnalytics.adminGetDashboard / getDocPageAnalytics queries
    DB-->>Server: stats, series, top links
    Gateway->>Gateway: every 60s flushActiveUsersMinute()
    Gateway->>DB: upsert into sync_active_users_minutely(active_users, minute_ts)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Charts in burrows, numbers in stew,

Views hop in, unique and new,
Minute and daily, counts that sing,
Dashboards sparkle—metrics spring,
A rabbit cheers for data true.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: doc status & share status' is vague and does not clearly convey the main purpose of this changeset. The PR introduces comprehensive analytics functionality including database migrations, new GraphQL types, resolvers, and frontend components for workspace and document analytics—not specifically about 'doc status' or 'share status'. Consider revising the title to more accurately reflect the primary changes, such as 'feat: add workspace and document analytics infrastructure' or 'feat: implement admin analytics and doc page analytics dashboard'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch darksky/doc-sharedlink-status

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added mod:i18n Related to i18n app:server test Related to test cases app:core labels Feb 12, 2026
@codecov
Copy link

codecov bot commented Feb 12, 2026

Codecov Report

❌ Patch coverage is 86.53433% with 251 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.49%. Comparing base (3ad4823) to head (3233746).
⚠️ Report is 2 commits behind head on canary.

Files with missing lines Patch % Lines
...s/backend/server/src/models/workspace-analytics.ts 87.78% 133 Missing and 6 partials ⚠️
...es/backend/server/src/core/workspaces/stats.job.ts 4.76% 40 Missing ⚠️
packages/backend/server/src/core/sync/gateway.ts 72.97% 30 Missing ⚠️
...ackend/server/src/core/workspaces/resolvers/doc.ts 91.27% 13 Missing ⚠️
...kend/server/src/core/workspaces/resolvers/admin.ts 95.58% 12 Missing ⚠️
...ages/backend/server/src/base/graphql/pagination.ts 75.00% 5 Missing ⚠️
...backend/server/src/core/doc-renderer/controller.ts 84.00% 4 Missing ⚠️
...s/backend/server/src/core/workspaces/controller.ts 89.47% 4 Missing ⚠️
...r/src/core/workspaces/resolvers/analytics-types.ts 93.54% 2 Missing ⚠️
...ages/workspace/detail-page/tabs/analytics.utils.ts 87.50% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           canary   #14426      +/-   ##
==========================================
- Coverage   53.58%   53.49%   -0.09%     
==========================================
  Files        2828     2831       +3     
  Lines      151202   153049    +1847     
  Branches    22806    22897      +91     
==========================================
+ Hits        81018    81877     +859     
- Misses      66973    67997    +1024     
+ Partials     3211     3175      -36     
Flag Coverage Δ
server-test 75.06% <86.36%> (-0.70%) ⬇️
unittest 32.67% <91.30%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In
`@packages/backend/server/migrations/20260212053401_workspace_analytics/migration.sql`:
- Around line 34-36: Remove the redundant explicit index creation for
"workspace_doc_view_daily_workspace_id_doc_id_date_idx" because the table's
primary key (workspace_id, doc_id, date) already creates an equivalent unique
index; delete the CREATE INDEX IF NOT EXISTS line that defines
workspace_doc_view_daily_workspace_id_doc_id_date_idx and keep the other index
for ("workspace_id", "date") intact.

In `@packages/backend/server/src/core/sync/gateway.ts`:
- Around line 348-366: The method flushActiveUsersMinute is misleading because
this.socketRedis.scard(this.activeConnectionSetKey) counts socket connections
(client IDs), not unique users; either change the implementation to record
unique user IDs (use the auth handshake user identifier when adding/removing
from the Redis set and call a new upsert method like
upsertSyncActiveUniqueUsersMinute) or explicitly rename symbols to reflect
"connections" (rename flushActiveUsersMinute to flushActiveConnectionsMinute,
activeConnectionSetKey to activeConnectionSetKeyConnections or similar, and
update the model call from upsertSyncActiveUsersMinute to
upsertSyncActiveConnectionsMinute) and update all call sites accordingly (refer
to method flushActiveUsersMinute, property activeConnectionSetKey,
this.socketRedis.scard usage, and
this.models.workspaceAnalytics.upsertSyncActiveUsersMinute).
- Around line 212-213: The shared Redis set activeConnectionSetKey is causing
stale entries after crashes; change from a plain set to a sorted set (ZSET) and
update the periodic flush (triggered by flushTimer) to ZADD each client.id with
score = current timestamp and then ZREMRANGEBYSCORE to remove entries older than
your staleness threshold (e.g., now - 2–3 minutes) before reading counts; update
any reads that used SCARD to use ZCARD (or ZCOUNT with the recent range) and
ensure the flushTimer handler performs the trim and heartbeat ZADD so crashed
instances' ids age out automatically.

In `@packages/backend/server/src/models/workspace-analytics.ts`:
- Around line 1037-1079: In buildSharedLinkCursorCondition, validate the
incoming cursor values before converting/using them: for ViewsDesc ensure
cursor.sortValue is a finite number (use Number and Number.isFinite) and for
date-based orders (PublishedAtDesc/UpdatedAtDesc) parse cursor.sortValue into a
Date and check !isNaN(date.valueOf()); if validation fails throw a BadRequest
error (or propagate a validation error) instead of building SQL with invalid
params; apply the same guard logic in paginateDocLastAccessedMembers for
MemberCursor to validate member cursor sortValue before using it.

In `@packages/frontend/admin/src/modules/dashboard/index.tsx`:
- Around line 48-77: The dashboard date/time formatters (formatDateTime and
formatDate) currently use the environment locale which applies the local
timezone; update these functions to call toLocaleString / toLocaleDateString
with an explicit timeZone: 'UTC' (and appropriate format options if needed) so
displayed timestamps align with the UTC sampling window described in the UI.

In `@packages/frontend/i18n/src/i18n.gen.ts`:
- Around line 4577-4644: The generated i18n type defines new analytics keys
(e.g., "com.affine.doc.analytics.title",
"com.affine.doc.analytics.summary.total",
"com.affine.doc.analytics.window.last-days",
"com.affine.doc.analytics.metric.total",
"com.affine.doc.analytics.metric.unique",
"com.affine.doc.analytics.metric.guest",
"com.affine.doc.analytics.chart.total-views",
"com.affine.doc.analytics.chart.unique-views",
"com.affine.doc.analytics.error.load-analytics",
"com.affine.doc.analytics.error.load-viewers",
"com.affine.doc.analytics.empty.no-page-views",
"com.affine.doc.analytics.empty.no-viewers",
"com.affine.doc.analytics.viewers.title",
"com.affine.doc.analytics.viewers.show-all",
"com.affine.doc.analytics.paywall.open-pricing",
"com.affine.doc.analytics.paywall.toast") but those keys are missing from the 24
non-English locale resource files; add the same keys to every locale JSON (ar,
ca, da, de, el-GR, es-AR, es-CL, es, fa, fr, hi, it-IT, it, ja, ko, nb-NO, pl,
pt-BR, ru, sv-SE, uk, ur, zh-Hans, zh-Hant) populating values either with the
proper translations or, temporarily, the English strings from en.json; after
updating the locale files, run the i18n build/generation step so types and
bundles reflect the new keys and verify no fallback to raw keys occurs at
runtime.
🧹 Nitpick comments (8)
packages/backend/server/src/core/sync/gateway.ts (2)

302-326: Inconsistent floating-promise handling between handleConnection and handleDisconnect.

handleDisconnect (line 320) prefixes the call with void to suppress the no-floating-promises lint rule, but handleConnection (line 306) does not. Both are fire-and-forget. Add void on line 306 for consistency and to avoid lint warnings.

Proposed fix
   handleConnection(client: Socket) {
     this.connectionCount++;
     this.logger.debug(`New connection, total: ${this.connectionCount}`);
     metrics.socketio.gauge('connections').record(this.connectionCount);
-    this.onConnectionPresenceChanged(client, 'add').catch(error => {
+    void this.onConnectionPresenceChanged(client, 'add').catch(error => {
       this.logger.warn(
         'Failed to update connection presence on add',
         error as Error
       );
     });
   }

345-346: flushActiveUsersMinute is called on every connect/disconnect — may be noisy under churn.

Each invocation performs a Redis scard + a DB upsert. Under high connection churn (e.g., many short-lived connections), this can produce a significant volume of DB writes per minute, all to the same minute-truncated row.

Consider debouncing or throttling so the method runs at most once every few seconds, relying on the 60 s interval as the baseline. Alternatively, remove the call from onConnectionPresenceChanged entirely and let the timer be the sole flush trigger.

Also applies to: 362-365

packages/frontend/admin/src/components/ui/chart.tsx (1)

38-71: dangerouslySetInnerHTML used for CSS injection — acceptable but consider hardening config keys.

The CSS is built from developer-supplied ChartConfig keys and color values, so XSS risk is low in practice. However, if a config key ever contained characters like ", }, or <, it could break out of the CSS/style context. A lightweight safeguard would be to sanitize keys (e.g., restrict to [a-zA-Z0-9_-]).

This is a well-known pattern (e.g., shadcn/ui charts), so flagging as optional.

🛡️ Optional: sanitize config keys
  const colorEntries = Object.entries(config).filter(
-    ([, item]) => item.color || item.theme
+    ([key, item]) => /^[a-zA-Z0-9_-]+$/.test(key) && (item.color || item.theme)
  );
packages/backend/server/migrations/20260212053401_workspace_analytics/migration.sql (1)

67-75: Legacy cleanup DROP CONSTRAINT / DROP INDEX lack IF EXISTS guards.

Unlike the CREATE TABLE IF NOT EXISTS and DROP TRIGGER IF EXISTS / DROP FUNCTION IF EXISTS statements earlier in this migration, the ALTER TABLE ... DROP CONSTRAINT and DROP INDEX statements on lines 67-75 will fail if the constraints or indexes don't exist. If this migration needs to be idempotent or re-runnable (e.g., during development), consider adding guards.

🛡️ Suggested: add IF EXISTS guards
 ALTER TABLE
-  "user_features" DROP CONSTRAINT "user_features_feature_id_fkey";
+  "user_features" DROP CONSTRAINT IF EXISTS "user_features_feature_id_fkey";
 
 ALTER TABLE
-  "workspace_features" DROP CONSTRAINT "workspace_features_feature_id_fkey";
+  "workspace_features" DROP CONSTRAINT IF EXISTS "workspace_features_feature_id_fkey";
 
-DROP INDEX "user_features_feature_id_idx";
+DROP INDEX IF EXISTS "user_features_feature_id_idx";
 
-DROP INDEX "workspace_features_feature_id_idx";
+DROP INDEX IF EXISTS "workspace_features_feature_id_idx";
packages/backend/server/src/core/workspaces/stats.job.ts (1)

323-348: CURRENT_DATE is server-timezone-dependent — worth documenting.

CURRENT_DATE uses the PostgreSQL server's timezone setting. If you ever need consistent date bucketing across environments (e.g., UTC), consider using CURRENT_DATE AT TIME ZONE 'UTC' or (NOW() AT TIME ZONE 'UTC')::date. This is fine if the server timezone is already set to UTC (common in cloud deployments), but worth a brief inline comment for future maintainers.

packages/backend/server/src/core/workspaces/controller.ts (1)

149-164: Consider merging the duplicate !docId.isWorkspace guard.

Lines 149 and 166 both check !docId.isWorkspace. While they serve different purposes (analytics vs. publish-mode header), combining them into a single block would avoid the small overhead of a redundant condition and make the flow easier to follow.

♻️ Suggested refactor
-    if (!docId.isWorkspace) {
-      void this.models.workspaceAnalytics
-        .recordDocView({
-          workspaceId: docId.workspace,
-          docId: docId.guid,
-          userId: user?.id,
-          visitorId: this.buildVisitorId(req, docId.workspace, docId.guid),
-          isGuest: !user,
-        })
-        .catch(error => {
-          this.logger.warn(
-            `Failed to record doc view: ${docId.workspace}/${docId.guid}`,
-            error as Error
-          );
-        });
-    }
-
-    if (!docId.isWorkspace) {
+    if (!docId.isWorkspace) {
+      void this.models.workspaceAnalytics
+        .recordDocView({
+          workspaceId: docId.workspace,
+          docId: docId.guid,
+          userId: user?.id,
+          visitorId: this.buildVisitorId(req, docId.workspace, docId.guid),
+          isGuest: !user,
+        })
+        .catch(error => {
+          this.logger.warn(
+            `Failed to record doc view: ${docId.workspace}/${docId.guid}`,
+            error as Error
+          );
+        });
+
       // fetch the publish page mode for publish page
packages/backend/server/schema.prisma (1)

328-383: Consider dropping the redundant PK index.
WorkspaceDocViewDaily already has @@id([workspaceId, docId, date]), so @@index([workspaceId, docId, date]) duplicates the PK index and adds write overhead.

♻️ Suggested cleanup
   @@id([workspaceId, docId, date])
   @@index([workspaceId, date])
-  @@index([workspaceId, docId, date])
   @@map("workspace_doc_view_daily")
packages/backend/server/src/models/workspace-analytics.ts (1)

442-453: Prefer either cursor or offset pagination, not both.
Using offset alongside after can skip/duplicate rows. Consider normalizing offset to 0 when a cursor is present (and doing the same in paginateDocLastAccessedMembers).

♻️ Suggested normalization
-    const pagination: PaginationInput = {
-      ...options.pagination,
-      first: Math.min(Math.max(options.pagination.first ?? 10, 1), 100),
-      offset: Math.max(options.pagination.offset ?? 0, 0),
-    };
+    const pagination: PaginationInput = {
+      ...options.pagination,
+      first: Math.min(Math.max(options.pagination.first ?? 10, 1), 100),
+      offset: options.pagination.after
+        ? 0
+        : Math.max(options.pagination.offset ?? 0, 0),
+    };

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/backend/server/src/core/sync/gateway.ts`:
- Around line 348-390: The upsert in
models.workspaceAnalytics.upsertSyncActiveUsersMinute allows last-write-wins
causing smaller values to overwrite larger ones within the same minute; update
the upsert SQL to use GREATEST(active_users, EXCLUDED.active_users) so the
stored value only increases within that minute, or alternately ensure only the
periodic aggregator (flushActiveUsersMinute with aggregateAcrossCluster=true)
performs DB writes and stop calling upsert from connect/disconnect paths—locate
calls to flushActiveUsersMinute and the upsert implementation in
workspaceAnalytics to apply the chosen change.
🧹 Nitpick comments (9)
packages/backend/server/src/__tests__/e2e/workspace/admin-analytics.spec.ts (1)

13-58: Prefer $executeRaw (tagged template) over $executeRawUnsafe for static SQL.

All four DDL statements are static string literals with no interpolation. Using the tagged template form ($executeRaw\...``) is more consistent with the rest of this file and avoids accidentally introducing injection if the strings are ever parameterized later.

♻️ Example for the first statement
 async function ensureAnalyticsTables(db: PrismaClient) {
-  await db.$executeRawUnsafe(`
+  await db.$executeRaw`
     CREATE TABLE IF NOT EXISTS workspace_admin_stats_daily (
       workspace_id VARCHAR NOT NULL,
       date DATE NOT NULL,
       snapshot_size BIGINT NOT NULL DEFAULT 0,
       blob_size BIGINT NOT NULL DEFAULT 0,
       member_count BIGINT NOT NULL DEFAULT 0,
       updated_at TIMESTAMPTZ(3) NOT NULL DEFAULT NOW(),
       PRIMARY KEY (workspace_id, date)
-    );
-  `);
+    );
+  `;

Apply the same pattern to the remaining three statements.

packages/backend/server/src/__tests__/sync/gateway.spec.ts (2)

504-520: Test is timing-sensitive and lacks an explicit value assertion.

Two observations:

  1. The test's correctness depends on the server flushing active-user counts to the DB within the 5 s WS_TIMEOUT_MS window. If the flush interval is increased or delayed under CI load, this test becomes flaky. Consider either triggering a flush explicitly or making the timeout configurable/generous for this specific case.

  2. t.pass() only proves waitForActiveUsers didn't throw. A direct assertion (e.g., t.is(await latestActiveUsers(db), 1)) after the wait would make the test output more informative on failure.

Minor: add explicit assertion after wait
     await Promise.all([waitForConnect(first), waitForConnect(second)]);
     await waitForActiveUsers(db, 1);
-    t.pass();
+    t.is(await latestActiveUsers(db), 1, 'expected exactly 1 active user after dedup');

150-158: Remove redundant ensureSyncActiveUsersTable — table is already created by migrations.

The hardcoded DDL matches the migration schema exactly, but the call at line 505 is unnecessary since initTestingDB() only truncates tables and preserves existing schema created by app initialization. If this defensive pattern is preferred, document why it's needed; otherwise, rely on migrations to set up the table structure.

packages/backend/server/src/core/sync/gateway.ts (1)

311-322: Disconnect flush uses local connection count, potentially underreporting.

With aggregateAcrossCluster: false, the disconnect handler writes this.connectionCount (local to this instance) as the active user count for the minute. In a multi-instance deployment, this will drastically underreport. Since the 60s interval corrects this, the impact is limited to a brief inaccuracy, but the DB write on disconnect is arguably misleading.

Consider skipping the DB write on disconnect entirely and relying solely on the periodic timer for accuracy, or always using aggregateAcrossCluster: true.

packages/backend/server/src/models/workspace-analytics.ts (3)

509-515: ILIKE wildcard characters in user-supplied keyword are not escaped.

Characters % and _ are LIKE wildcards. A keyword like % would match everything; _ would match any single character. Since this is an admin endpoint the risk is low, but for correctness you should escape these metacharacters.

♻️ Suggested helper
+function escapeLikePattern(pattern: string): string {
+  return pattern.replace(/[%_\\]/g, '\\$&');
+}
+
 // then in the query:
-  ILIKE ${`%${keyword}%`}
+  ILIKE ${`%${escapeLikePattern(keyword)}%`}

1120-1137: Redis failure silently overcounts unique visitors.

When sadd throws (line 1134), the catch returns true, meaning the view is counted as unique even if we couldn't verify. This is a reasonable choice for availability-over-accuracy, but the failure is completely silent — no log, no metric.

Consider adding a warning log or incrementing an error counter so operational dashboards can detect Redis connectivity issues affecting analytics accuracy.

📝 Proposed change
-    } catch {
+    } catch (error) {
+      this.logger.warn?.('Failed to check unique visitor in Redis, assuming unique', error);
       return true;
     }

Note: BaseModel may not have a logger — if not, use a static logger or inject one.


991-1028: Duplicated filter logic between countAdminSharedLinks and adminPaginateAllSharedLinks.

The keyword, workspace, and updatedAfter conditions are reimplemented in both the pagination query and this count query. If filters diverge, the count will become inconsistent with the paginated results. Consider extracting the shared WHERE clause into a helper that both methods can reuse.

packages/backend/server/schema.prisma (2)

343-349: SyncActiveUsersMinutely will grow unbounded — consider a retention strategy.

At one row per minute, this table accumulates ~525K rows/year. The admin dashboard only queries the last 48–72 hours. Without periodic cleanup (e.g., a cron job, pg_cron, or a DB-level partitioning/retention policy), this table will grow indefinitely.

Consider adding a periodic job to prune rows older than a configurable threshold (e.g., 7 or 30 days).


368-382: Consider a composite index to cover the member-access query's ORDER BY.

The paginateDocLastAccessedMembers query filters on (workspace_id, last_doc_id) and orders by (last_accessed_at DESC, user_id ASC). The current @@index([workspaceId, lastDocId]) supports the filter, but Postgres will need a separate sort step for the ORDER BY. For large member lists, a composite index could eliminate this:

@@index([workspaceId, lastDocId, lastAccessedAt(sort: Desc)])

This is unlikely to matter with the LIMIT 50 and for typical workspace sizes, so flagging this as optional.

@darkskygit darkskygit merged commit b4be911 into canary Feb 12, 2026
124 of 126 checks passed
@darkskygit darkskygit deleted the darksky/doc-sharedlink-status branch February 12, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:core app:server mod:i18n Related to i18n test Related to test cases

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant