Skip to content

fix: fully honor block-anonymous-tracking flag on client and server#41973

Open
salevine wants to merge 5 commits into
releasefrom
chore/15380/usage_info
Open

fix: fully honor block-anonymous-tracking flag on client and server#41973
salevine wants to merge 5 commits into
releasefrom
chore/15380/usage_info

Conversation

@salevine

@salevine salevine commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Description

Makes the configure_block_event_tracking_for_anonymous_users feature flag completely turn off anonymous-user event tracking. Two gaps let anonymous events through when the flag was on:

Gap 1 — client license bypass (app/client/src/ce/sagas/userSagas.tsx)

shouldTrackUser short-circuited on an active license:

isAnonymous && (licenseActive || (telemetryOn && !featureFlag))

so licensed EE/cloud instances tracked anonymous users regardless of the flag. Removed the licenseActive bypass — the flag is now honored on all instances.

Gap 2 — ungated direct sendEvent path (AnalyticsServiceCEImpl)

The flag was only checked in sendObjectEvent. Direct callers of sendEvent(event, userId, props) with an anonymous userId bypassed it (and that path resolves the anonymous user to the client x-anonymous-user-id). Added a gate on the public sendEvent that blocks anonymous userIds before id resolution, and it fails closed if the flag state can't be resolved (drops the event, completes the chain). The existing sendObjectEvent check is kept (it governs the session-user/signup path, where sendEvent receives the new user's email rather than "anonymousUser"). sendObjectEvent now routes through the extracted private sendEventInternal to avoid a redundant (Redis-backed) flag check on the hot anonymous path (published-app page views / action executions).

Scope

  • Behavior is opt-in: the flag defaults to false, so existing instances are unchanged until an admin enables it.
  • Out of scope (intentional): the usage-pulse channel (/api/v1/usage-pulse) still emits the anonymous ID — that feeds billing/seat metering, not Segment analytics, and is treated separately.

Tests

  • Client — new userSagas.test.ts covering the shouldTrackUser matrix, including the regression guard that anonymous + telemetry-on + flag-on returns false on a licensed instance.
  • Server — new AnalyticsServiceCEImplTest cases: a direct anonymous sendEvent enqueues nothing when the flag is on, and fails closed (no enqueue, no error) when the flag check errors.

Ticket: #15380

Automation

/ok-to-test tags="@tag.All"

🔍 Cypress test results

Tip

🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
Workflow run: https://github.com/appsmithorg/appsmith/actions/runs/29952940414
Commit: c1cf491
Cypress dashboard.
Tags: @tag.All
Spec:


Wed, 22 Jul 2026 21:15:04 UTC

Communication

Should the DevRel and Marketing teams inform users about this change?

  • Yes
  • No

Summary by CodeRabbit

  • New Features

    • Improved anonymous-user analytics controls using the configured tracking restriction.
    • Anonymous analytics events are now blocked safely when tracking is restricted or the setting cannot be checked.
    • Usage pulse data now maintains a stable anonymous identifier, including when telemetry settings change or local storage is unavailable.
  • Bug Fixes

    • Corrected anonymous-user detection and prevented unintended event tracking.
    • Preserved usage pulse reporting when external or local identifier retrieval fails.
  • Tests

    • Added coverage for anonymous tracking decisions, event suppression, and anonymous identifier fallback behavior.

The configure_block_event_tracking_for_anonymous_users flag left two gaps
that let anonymous-user events through when it was on:

- Client (shouldTrackUser): an active license short-circuited the flag,
  so licensed EE/cloud instances always tracked anonymous users. Removed
  the licenseActive bypass so the flag is honored on all instances.
- Server (AnalyticsServiceCEImpl.sendEvent): the flag was only checked in
  sendObjectEvent. Direct sendEvent callers passing an anonymous userId
  bypassed it. Added a gate on the public sendEvent that blocks anonymous
  userIds before the x-anonymous-user-id is resolved, keeping the existing
  sendObjectEvent check for the session-user (signup) path. sendObjectEvent
  now routes through the extracted private sendEventInternal to avoid a
  redundant (Redis-backed) flag check on the hot anonymous path.

usage-pulse billing/seat metering is intentionally out of scope.

Adds unit tests: client shouldTrackUser matrix (incl. the license-bypass
regression guard) and a server sendEvent anonymous-block guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@salevine salevine added the ok-to-test Required label for CI label Jul 9, 2026
@github-actions github-actions Bot added the Bug Something isn't working label Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Whoops! Looks like you're using an outdated method of running the Cypress suite.
Please check this doc to learn how to correct this!

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Anonymous-user analytics tracking now uses configure_block_event_tracking_for_anonymous_users on the client and server. Usage pulse payloads now use stable, storage-safe anonymous fallback IDs, with tests covering tracking decisions, event suppression, telemetry behavior, and fallback reuse.

Changes

Anonymous User Analytics Gating

Layer / File(s) Summary
Client tracking decision
app/client/src/ce/sagas/userSagas.tsx, app/client/src/ce/sagas/userSagas.test.ts
shouldTrackUser uses ANONYMOUS_USERNAME, accepts the blocking feature flag, and removes the license-based exception; tests cover the updated decisions.
Server anonymous event gating
app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java, app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AnalyticsServiceCEImplTest.java
Anonymous events are suppressed when the flag blocks or fails to resolve, and sendObjectEvent avoids duplicate gating; tests verify Segment is not called.

Usage Pulse Anonymous Identity

Layer / File(s) Summary
Usage pulse fallback identity
app/client/src/usagePulse/utils.ts, app/client/src/usagePulse/utils.test.ts
Anonymous payloads use a persisted fallback ID when available, tolerate storage failures, prefer Segment IDs when telemetry is enabled, and reuse the fallback across pulses.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AnalyticsServiceCEImpl
  participant FeatureFlagService
  participant SegmentAnalytics

  Client->>AnalyticsServiceCEImpl: send anonymous analytics event
  AnalyticsServiceCEImpl->>FeatureFlagService: check anonymous tracking flag
  FeatureFlagService-->>AnalyticsServiceCEImpl: blocked, allowed, or error
  alt blocked or unresolved
    AnalyticsServiceCEImpl-->>Client: complete without sending
  else allowed
    AnalyticsServiceCEImpl->>SegmentAnalytics: enqueue event
  end
Loading

Suggested reviewers: subrata71

Poem

A quiet flag guards the trail,
Fallback IDs remain when signals fail.
Blocked events fade from view,
Tests confirm the gate holds true.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: fully honoring the block-anonymous-tracking flag on client and server.
Description check ✅ Passed The description covers motivation, scope, tests, issue link, automation, and communication, matching the template well.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/15380/usage_info

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

…S_USERNAME

Code-review follow-ups on the anonymous-tracking gate:

- sendEvent's anonymous gate now handles featureFlagService.check()
  errors with onErrorResume, failing closed (event dropped, chain
  completes) instead of propagating the error into previously
  fire-and-forget callers like GlobalExceptionHandler. Adds a
  regression test for the error path.
- Align gate style with repo convention: raw if (isBlocked) and plain
  Mono.empty() (Reactor forbids null onNext, so the defensive forms
  bought nothing).
- Client: use the existing ANONYMOUS_USERNAME constant from
  userConstants instead of the hardcoded "anonymousUser" literal in
  shouldTrackUser and its test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@salevine

salevine commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

/ok-to-test tags="@tag.All"

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Whoops! Looks like you're using an outdated method of running the Cypress suite.
Please check this doc to learn how to correct this!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AnalyticsServiceCEImplTest.java (2)

55-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the ANONYMOUS_USERNAME constant instead of a hardcoded string.

The PR objectives note that the client was updated to use ANONYMOUS_USERNAME instead of a hardcoded "anonymousUser" string. The tests should reference the same server-side constant (if one exists) to avoid drift if the value ever changes.

Run the following script to check for an existing constant:

#!/bin/bash
# Description: Search for an ANONYMOUS_USERNAME or similar constant on the server side.
rg -n 'ANONYMOUS_USERNAME|ANONYMOUS_USER' --type java app/server/appsmith-server/src/main/java/com/appsmith/server/

Also applies to: 73-73

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AnalyticsServiceCEImplTest.java`
at line 55, The test is using a hardcoded anonymous username string instead of
the shared server constant, so update AnalyticsServiceCEImplTest to reference
the existing ANONYMOUS_USERNAME symbol wherever sendEvent is called with the
anonymous user. Check the server-side constant location first and use that value
consistently in the test methods so the assertions stay aligned with the
production contract.

41-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a positive test case for when the flag is disabled.

Both new tests cover the blocking paths (flag on, flag error). A third test where the flag resolves to false (don't block) and asserting that enqueue is called would confirm the gate works bidirectionally and strengthen confidence that the blocking tests aren't trivially passing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AnalyticsServiceCEImplTest.java`
around lines 41 - 77, Add a positive regression test in
AnalyticsServiceCEImplTest for the anonymous-user path when
FeatureFlagService.check(configure_block_event_tracking_for_anonymous_users)
resolves to false, so the gate does not short-circuit. Reuse
AnalyticsServiceCEImpl with a mocked Analytics and FeatureFlagService, call
sendEvent with an anonymous user, and verify the returned Mono completes and
analytics.enqueue is invoked once. This should sit alongside the existing
sendEvent_anonymousUserWithBlockFlagOn_doesNotEnqueueEvent and
sendEvent_anonymousUserWhenFlagCheckErrors_completesWithoutEnqueueing tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AnalyticsServiceCEImplTest.java`:
- Line 55: The test is using a hardcoded anonymous username string instead of
the shared server constant, so update AnalyticsServiceCEImplTest to reference
the existing ANONYMOUS_USERNAME symbol wherever sendEvent is called with the
anonymous user. Check the server-side constant location first and use that value
consistently in the test methods so the assertions stay aligned with the
production contract.
- Around line 41-77: Add a positive regression test in
AnalyticsServiceCEImplTest for the anonymous-user path when
FeatureFlagService.check(configure_block_event_tracking_for_anonymous_users)
resolves to false, so the gate does not short-circuit. Reuse
AnalyticsServiceCEImpl with a mocked Analytics and FeatureFlagService, call
sendEvent with an anonymous user, and verify the returned Mono completes and
analytics.enqueue is invoked once. This should sit alongside the existing
sendEvent_anonymousUserWithBlockFlagOn_doesNotEnqueueEvent and
sendEvent_anonymousUserWhenFlagCheckErrors_completesWithoutEnqueueing tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dc6fb33f-56ff-432f-8ded-d9f5dc444e0d

📥 Commits

Reviewing files that changed from the base of the PR and between 8708b67 and dbf8f56.

📒 Files selected for processing (4)
  • app/client/src/ce/sagas/userSagas.test.ts
  • app/client/src/ce/sagas/userSagas.tsx
  • app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java
  • app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AnalyticsServiceCEImplTest.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/client/src/ce/sagas/userSagas.test.ts
  • app/client/src/ce/sagas/userSagas.tsx
  • app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java

@salevine salevine added ok-to-test Required label for CI and removed ok-to-test Required label for CI labels Jul 9, 2026
@salevine
salevine requested a review from subrata71 July 9, 2026 15:21

@subrata71 subrata71 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This blocks Segment initialization for anonymous users when configure_block_event_tracking_for_anonymous_users is enabled, which looks correct for analytics tracking.

But usage-pulse still depends on AnalyticsUtil.getAnonymousId() when enableTelemetry && segment.enabled is true. In this new state, segmentAnalytics exists but has no Segment user, so getAnonymousId() can return undefined, and /api/v1/usage-pulse may stop sending anonymousUserId.

Can we also update usage-pulse to fall back to its local FALLBACK_KEY/nanoid() ID when AnalyticsUtil.getAnonymousId() is unavailable? That would preserve the PR’s stated scope that usage-pulse remains separate from Segment analytics blocking. Also could you please verify how does the PR behave in the EE repo?

…nymous users

When configure_block_event_tracking_for_anonymous_users blocks Segment
init, segmentAnalytics exists but has no user, so
AnalyticsUtil.getAnonymousId() returns undefined. The telemetry-enabled
anonymous branch of getUsagePulsePayload then sent no anonymousUserId,
breaking anonymous usage counting.

Extract the existing local-fallback-id logic into
getOrCreateFallbackAnonymousId() and fall back to it whenever the Segment
id is unavailable, keeping usage-pulse's identity independent of Segment
analytics blocking. The helper also guards localStorage access so a
private-mode/quota throw returns a per-call id instead of dropping the
pulse.

Adds unit coverage for getUsagePulsePayload across the telemetry
on/off and Segment-available/unavailable branches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@salevine

Copy link
Copy Markdown
Contributor Author

Hardening: guard localStorage in the usage-pulse fallback id (commit b23ab45)

Follow-up to the usage-pulse fallback change. getOrCreateFallbackAnonymousId() in app/client/src/usagePulse/utils.ts writes to localStorage, which can throw when storage is unavailable (private/incognito mode, quota exceeded, or storage disabled by policy). Because this helper is now reached on the telemetry-enabled anonymous path (not just the telemetry-disabled one), an unguarded throw would propagate up through getUsagePulsePayloadUsagePulse.sendPulse and drop the pulse entirely.

Fix: wrap the read/write in a try/catch. On failure we return a non-persisted per-call nanoid() so the pulse still carries an anonymousUserId instead of failing. The trade-off is that the id isn't stable across pulses in that degraded environment — acceptable, since the alternative is losing the pulse.

const getOrCreateFallbackAnonymousId = (): string => {
  try {
    let fallback = localStorage.getItem(FALLBACK_KEY);
    if (!fallback) {
      fallback = nanoid();
      localStorage.setItem(FALLBACK_KEY, fallback);
    }
    return fallback;
  } catch {
    // localStorage unavailable (private mode, quota, disabled) — return a
    // non-persisted per-call id so the pulse still carries an anonymousUserId.
    return nanoid();
  }
};

Covered by a new unit test (utils.test.ts) that mocks Storage.prototype.setItem to throw and asserts a non-empty anonymousUserId is still produced.

Verification: yarn jest src/usagePulse/utils.test.ts src/usagePulse/usagePulse.test.ts → 11/11 pass; yarn run check-types and yarn eslint on the changed files both clean.

@salevine

Copy link
Copy Markdown
Contributor Author

This blocks Segment initialization for anonymous users when configure_block_event_tracking_for_anonymous_users is enabled, which looks correct for analytics tracking.

But usage-pulse still depends on AnalyticsUtil.getAnonymousId() when enableTelemetry && segment.enabled is true. In this new state, segmentAnalytics exists but has no Segment user, so getAnonymousId() can return undefined, and /api/v1/usage-pulse may stop sending anonymousUserId.

Can we also update usage-pulse to fall back to its local FALLBACK_KEY/nanoid() ID when AnalyticsUtil.getAnonymousId() is unavailable? That would preserve the PR’s stated scope that usage-pulse remains separate from Segment analytics blocking. Also could you please verify how does the PR behave in the EE repo?

@subrata71 - I updated it to address this

@salevine
salevine requested a review from subrata71 July 10, 2026 14:05
@subrata71

Copy link
Copy Markdown
Collaborator

This blocks Segment initialization for anonymous users when configure_block_event_tracking_for_anonymous_users is enabled, which looks correct for analytics tracking.
But usage-pulse still depends on AnalyticsUtil.getAnonymousId() when enableTelemetry && segment.enabled is true. In this new state, segmentAnalytics exists but has no Segment user, so getAnonymousId() can return undefined, and /api/v1/usage-pulse may stop sending anonymousUserId.
Can we also update usage-pulse to fall back to its local FALLBACK_KEY/nanoid() ID when AnalyticsUtil.getAnonymousId() is unavailable? That would preserve the PR’s stated scope that usage-pulse remains separate from Segment analytics blocking. Also could you please verify how does the PR behave in the EE repo?

@subrata71 - I updated it to address this

@salevine I don't see any recent changes which addresses this. Forgot to push maybe?

@github-actions

Copy link
Copy Markdown

This PR has not seen activitiy for a while. It will be closed in 7 days unless further activity is detected.

@github-actions github-actions Bot added the Stale label Jul 18, 2026
@subrata71

Copy link
Copy Markdown
Collaborator

@salevine just checking in here. I still don’t see the usage-pulse fallback commit on this PR as mentioned above.

@salevine
salevine requested a review from a team as a code owner July 22, 2026 14:24
@hacktron-app

hacktron-app Bot commented Jul 22, 2026

Copy link
Copy Markdown

Hacktron Security Check - Skipped

Reason: Billing required for Code Review seats

Add a payment method and start Code Review seat billing in organization billing settings

Go to: https://app.hacktron.ai/appsmith-inc/billing

@salevine

Copy link
Copy Markdown
Contributor Author

@subrata71 I re-checked this against the current PR head.

Your usage-pulse concern was valid before the last push: when anonymous Segment tracking is blocked, AnalyticsUtil.getAnonymousId() can be undefined, so /api/v1/usage-pulse needed to fall back to usage-pulse's own FALLBACK_KEY / nanoid() id.

The missing commit is now pushed on this PR: b23ab45479 (fix: keep usage-pulse anonymousUserId when Segment is blocked for anonymous users). It updates app/client/src/usagePulse/utils.ts to prefer Segment's id when available and fall back to the locally persisted usage-pulse id when it is not. It also guards localStorage failures so the pulse still carries an anonymousUserId.

EE verification: I checked the EE client shape as well. There is no separate EE usagePulse implementation; app/client/src/usagePulse/index.ts calls the shared getUsagePulsePayload, and ee/constants/UsagePulse re-exports the CE constants. So the same fallback path applies in EE once this client change is present there.

Local verification:

  • yarn jest src/usagePulse/utils.test.ts src/usagePulse/usagePulse.test.ts -> 11/11 passing
  • yarn eslint src/usagePulse/utils.ts src/usagePulse/utils.test.ts -> passing

@github-actions github-actions Bot removed the Stale label Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working ok-to-test Required label for CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants