chore: Disable anonymous user tracking when feature flag turned on (#40936)

## Description
> [!TIP]  
> _Add a TL;DR when the description is longer than 500 words or
extremely technical (helps the content, marketing, and DevRel team)._
>
> _Please also include relevant motivation and context. List any
dependencies that are required for this change. Add links to Notion,
Figma or any other documents that might be relevant to the PR._


Fixes #`Issue Number`  
_or_  
Fixes `Issue URL`
> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

/test sanity

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/15996654359>
> Commit: ba524fe2f769b6f8d2c72e0332560dd5e0c0465e
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=15996654359&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Sanity`
> Spec:
> <hr>Tue, 01 Jul 2025 11:08:01 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced a feature flag to control event tracking for anonymous
users.
* Added the ability to block analytics event tracking for anonymous
users when the feature flag is enabled.

* **Bug Fixes**
* Improved logic to ensure analytics events are not sent for anonymous
users if the feature flag is active.

* **Chores**
* Updated analytics initialization to respect the new tracking
preference for anonymous users.
* Enhanced tracking initialization flow to conditionally enable or
disable analytics based on user status and feature flag.
* Added tests to verify analytics initialization respects user tracking
preferences.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: jacquesikot <jacquesikot@gmail.com>
This commit is contained in:
Trisha Anand 2025-07-02 12:50:51 +05:30 committed by GitHub
parent be3aef333b
commit 481988daf1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 106 additions and 20 deletions

View File

@ -62,6 +62,8 @@ export const FEATURE_FLAG = {
license_ai_agent_instance_enabled: "license_ai_agent_instance_enabled",
release_jsobjects_onpageunloadactions_enabled:
"release_jsobjects_onpageunloadactions_enabled",
configure_block_event_tracking_for_anonymous_users:
"configure_block_event_tracking_for_anonymous_users",
} as const;
export type FeatureFlag = keyof typeof FEATURE_FLAG;
@ -113,6 +115,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
release_reactive_actions_enabled: false,
license_ai_agent_instance_enabled: false,
release_jsobjects_onpageunloadactions_enabled: false,
configure_block_event_tracking_for_anonymous_users: false,
};
export const AB_TESTING_EVENT_KEYS = {

View File

@ -7,6 +7,7 @@ import {
take,
type TakeEffect,
} from "redux-saga/effects";
import type { SagaIterator } from "redux-saga";
import type {
ReduxAction,
ReduxActionWithPromise,
@ -88,6 +89,7 @@ import {
segmentInitUncertain,
} from "actions/analyticsActions";
import { getSegmentState } from "selectors/analyticsSelectors";
import { getOrganizationConfig } from "ee/selectors/organizationSelectors";
export function* getCurrentUserSaga(action?: {
payload?: { userProfile?: ApiResponse };
@ -150,7 +152,28 @@ function* getSessionRecordingConfig() {
};
}
function* initTrackers(currentUser: User) {
function shouldTrackUser(
currentUser: User,
licenseActive: boolean,
featureFlag: boolean,
): boolean {
try {
const isAnonymous =
currentUser?.isAnonymous || currentUser?.username === "anonymousUser";
if (!isAnonymous) {
return true;
}
const telemetryOn = currentUser?.enableTelemetry ?? false;
return isAnonymous && (licenseActive || (telemetryOn && !featureFlag));
} catch (error) {
return true;
}
}
function* initTrackers(currentUser: User): SagaIterator {
try {
const isFFFetched: boolean = yield select(getFeatureFlagsFetched);
@ -162,7 +185,21 @@ function* initTrackers(currentUser: User) {
getSessionRecordingConfig,
);
yield call(AnalyticsUtil.initialize, currentUser, sessionRecordingConfig);
const featureFlags: FeatureFlags = yield select(selectFeatureFlags);
const organizationConfig = yield select(getOrganizationConfig);
const shouldTrack = shouldTrackUser(
currentUser,
organizationConfig.license.active,
featureFlags.configure_block_event_tracking_for_anonymous_users,
);
yield call(
AnalyticsUtil.initialize,
currentUser,
sessionRecordingConfig,
shouldTrack,
);
yield put(segmentInitSuccess());
} catch (e) {
log.error(e);

View File

@ -30,13 +30,14 @@ let segmentAnalytics: SegmentSingleton | null = null;
async function initialize(
user: User,
sessionRecordingConfig: SessionRecordingConfig,
shouldTrackUser: boolean,
) {
// SentryUtil.init();
await SmartlookUtil.init();
segmentAnalytics = SegmentSingleton.getInstance();
await segmentAnalytics.init();
await segmentAnalytics.init(shouldTrackUser);
// Mixpanel needs to be initialized after Segment
await MixpanelSingleton.getInstance().init(sessionRecordingConfig);

View File

@ -58,7 +58,7 @@ describe("SegmentSingleton", () => {
describe("init", () => {
it("should initialize successfully with API key", async () => {
const segment = SegmentSingleton.getInstance();
const result = await segment.init();
const result = await segment.init(true);
expect(result).toBe(true);
expect(mockAnalyticsBrowser.load).toHaveBeenCalledWith(
@ -73,7 +73,15 @@ describe("SegmentSingleton", () => {
});
const segment = SegmentSingleton.getInstance();
const result = await segment.init();
const result = await segment.init(true);
expect(result).toBe(true);
expect(mockAnalyticsBrowser.load).not.toHaveBeenCalled();
});
it("should not initialize when shouldTrackUser is false", async () => {
const segment = SegmentSingleton.getInstance();
const result = await segment.init(false);
expect(result).toBe(true);
expect(mockAnalyticsBrowser.load).not.toHaveBeenCalled();
@ -89,7 +97,7 @@ describe("SegmentSingleton", () => {
});
const segment = SegmentSingleton.getInstance();
const result = await segment.init();
const result = await segment.init(true);
expect(result).toBe(true);
expect(mockAnalyticsBrowser.load).toHaveBeenCalledWith(
@ -119,7 +127,7 @@ describe("SegmentSingleton", () => {
const eventData = { test: "data" };
segment.track("test-event", eventData);
await segment.init();
await segment.init(true);
expect(mockAnalytics.track).toHaveBeenCalledWith("test-event", eventData);
});
@ -127,7 +135,7 @@ describe("SegmentSingleton", () => {
it("should track events directly when initialized", async () => {
const segment = SegmentSingleton.getInstance();
await segment.init();
await segment.init(true);
const eventData = { test: "data" };
@ -141,7 +149,7 @@ describe("SegmentSingleton", () => {
it("should call analytics identify when initialized", async () => {
const segment = SegmentSingleton.getInstance();
await segment.init();
await segment.init(true);
const userId = "test-user";
const traits = { name: "Test User" };
@ -156,7 +164,7 @@ describe("SegmentSingleton", () => {
it("should call analytics reset when initialized", async () => {
const segment = SegmentSingleton.getInstance();
await segment.init();
await segment.init(true);
segment.reset();
@ -169,7 +177,7 @@ describe("SegmentSingleton", () => {
mockAnalyticsBrowser.load.mockRejectedValueOnce(new Error("Init failed"));
const segment = SegmentSingleton.getInstance();
const result = await segment.init();
const result = await segment.init(true);
expect(result).toBe(false);
expect(log.error).toHaveBeenCalledWith(
@ -182,7 +190,7 @@ describe("SegmentSingleton", () => {
it("should not track events after avoidTracking is called", async () => {
const segment = SegmentSingleton.getInstance();
await segment.init();
await segment.init(true);
// Track an event before calling avoidTracking
segment.track("pre-avoid-event", { data: "value" });
@ -214,7 +222,7 @@ describe("SegmentSingleton", () => {
segment.avoidTracking();
// Initialize
await segment.init();
await segment.init(true);
// Analytics track should not be called since we're avoiding tracking
expect(mockAnalytics.track).not.toHaveBeenCalled();

View File

@ -49,7 +49,7 @@ class SegmentSingleton {
}
}
public async init(): Promise<boolean> {
public async init(shouldTrackUser: boolean): Promise<boolean> {
const { segment } = getAppsmithConfigs();
if (!segment.enabled) {
@ -58,6 +58,12 @@ class SegmentSingleton {
return true;
}
if (!shouldTrackUser) {
this.avoidTracking();
return true;
}
if (this.analytics) {
log.warn("Segment is already initialized.");

View File

@ -15,6 +15,7 @@ public enum FeatureFlagEnum {
release_embed_hide_share_settings_enabled,
rollout_datasource_test_rate_limit_enabled,
release_gs_all_sheets_options_enabled,
configure_block_event_tracking_for_anonymous_users,
/**
* Feature flag to detect if the git reset optimization is enabled
*/

View File

@ -9,6 +9,7 @@ import com.appsmith.server.services.ce.AnalyticsServiceCEImpl;
import com.segment.analytics.Analytics;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@Slf4j
@ -24,7 +25,8 @@ public class AnalyticsServiceImpl extends AnalyticsServiceCEImpl implements Anal
UserUtils userUtils,
ProjectProperties projectProperties,
UserDataRepository userDataRepository,
DeploymentProperties deploymentProperties) {
DeploymentProperties deploymentProperties,
@Lazy FeatureFlagService featureFlagService) {
super(
analytics,
sessionUserService,
@ -33,6 +35,7 @@ public class AnalyticsServiceImpl extends AnalyticsServiceCEImpl implements Anal
userUtils,
projectProperties,
deploymentProperties,
userDataRepository);
userDataRepository,
featureFlagService);
}
}

View File

@ -1,6 +1,7 @@
package com.appsmith.server.services.ce;
import com.appsmith.external.constants.AnalyticsEvents;
import com.appsmith.external.enums.FeatureFlagEnum;
import com.appsmith.external.helpers.Identifiable;
import com.appsmith.external.models.ActionDTO;
import com.appsmith.external.models.BaseDomain;
@ -15,6 +16,7 @@ import com.appsmith.server.helpers.ExchangeUtils;
import com.appsmith.server.helpers.UserUtils;
import com.appsmith.server.repositories.UserDataRepository;
import com.appsmith.server.services.ConfigService;
import com.appsmith.server.services.FeatureFlagService;
import com.appsmith.server.services.SessionUserService;
import com.segment.analytics.Analytics;
import com.segment.analytics.messages.IdentifyMessage;
@ -24,6 +26,7 @@ import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import reactor.core.publisher.Mono;
import java.util.HashMap;
@ -48,6 +51,7 @@ public class AnalyticsServiceCEImpl implements AnalyticsServiceCE {
private final SessionUserService sessionUserService;
private final CommonConfig commonConfig;
private final ConfigService configService;
private final FeatureFlagService featureFlagService;
private final UserUtils userUtils;
@ -65,11 +69,13 @@ public class AnalyticsServiceCEImpl implements AnalyticsServiceCE {
UserUtils userUtils,
ProjectProperties projectProperties,
DeploymentProperties deploymentProperties,
UserDataRepository userDataRepository) {
UserDataRepository userDataRepository,
@Lazy FeatureFlagService featureFlagService) {
this.analytics = analytics;
this.sessionUserService = sessionUserService;
this.commonConfig = commonConfig;
this.configService = configService;
this.featureFlagService = featureFlagService;
this.userUtils = userUtils;
this.projectProperties = projectProperties;
this.deploymentProperties = deploymentProperties;
@ -319,7 +325,26 @@ public class AnalyticsServiceCEImpl implements AnalyticsServiceCE {
Mono<User> userMono = sessionUserService.getCurrentUser().switchIfEmpty(Mono.just(anonymousUser));
return userMono.flatMap(user -> Mono.zip(
return userMono.flatMap(user -> {
// if the user is anonymous, check if the feature flag
// configure_block_event_tracking_for_anonymous_users is enabled. If yes, then do not send the
// analytics event.
if (user.isAnonymous()) {
return featureFlagService
.check(FeatureFlagEnum.configure_block_event_tracking_for_anonymous_users)
.flatMap(isDisabled -> {
if (isDisabled) {
log.debug("Analytics event {} is not sent for anonymous user", eventTag);
return Mono.empty();
} else {
return Mono.just(user);
}
});
}
return Mono.just(user);
})
.flatMap(user -> Mono.zip(
user.isAnonymous()
? ExchangeUtils.getAnonymousUserIdFromCurrentRequest()
: Mono.just(user.getUsername()),
@ -363,8 +388,10 @@ public class AnalyticsServiceCEImpl implements AnalyticsServiceCE {
analyticsProperties.remove(FieldName.CLOUD_HOSTED_EXTRA_PROPS);
}
return sendEvent(eventTag, username, analyticsProperties).thenReturn(object);
});
return sendEvent(eventTag, username, analyticsProperties);
})
// Return the original object after sending the event
.then(Mono.just(object));
}
/**