[privacy] make observability local-only

This commit is contained in:
Kunthawat Greethong
2026-08-16 02:29:29 +07:00
parent 372f3165ce
commit 19dc44969a
29 changed files with 96 additions and 541 deletions

View File

@@ -3,7 +3,7 @@
import { unref } from 'vue';
import camelcaseKeys from 'camelcase-keys';
import snakecaseKeys from 'snakecase-keys';
import * as Sentry from '@sentry/vue';
import ExceptionTracker from 'shared/helpers/ExceptionTracker';
/**
* Vue composable that converts object keys to camelCase
@@ -17,12 +17,12 @@ export function useCamelCase(payload, options) {
const unrefPayload = unref(payload);
return camelcaseKeys(unrefPayload, options);
} catch (e) {
Sentry.setContext('transform-keys-error', {
ExceptionTracker.setContext('transform-keys-error', {
payload,
options,
op: 'camelCase',
});
Sentry.captureException(e);
ExceptionTracker.captureException(e);
return payload;
}
}
@@ -39,12 +39,12 @@ export function useSnakeCase(payload, options) {
const unrefPayload = unref(payload);
return snakecaseKeys(unrefPayload, options);
} catch (e) {
Sentry.setContext('transform-keys-error', {
ExceptionTracker.setContext('transform-keys-error', {
payload,
options,
op: 'snakeCase',
});
Sentry.captureException(e);
ExceptionTracker.captureException(e);
return payload;
}
}

View File

@@ -6,7 +6,7 @@ import {
messageSchema,
Selection,
} from '@chatwoot/prosemirror-schema';
import * as Sentry from '@sentry/vue';
import ExceptionTracker from 'shared/helpers/ExceptionTracker';
import camelcaseKeys from 'camelcase-keys';
import { FORMATTING, MARKDOWN_PATTERNS } from 'dashboard/constants/editor';
import { INBOX_TYPES, TWILIO_CHANNEL_MEDIUM } from 'dashboard/helper/inbox';
@@ -117,7 +117,7 @@ export function cleanSignature(signature) {
} catch (e) {
// eslint-disable-next-line no-console
console.warn(e);
Sentry.captureException(e);
ExceptionTracker.captureException(e);
// The parser can break on some cases
// for example, Token type `hr` not supported by Markdown parser
return signature;

View File

@@ -16,7 +16,7 @@ import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
import { META_RESTRICTION_STATUS_URL } from 'dashboard/constants/globals';
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
import * as Sentry from '@sentry/vue';
import ExceptionTracker from 'shared/helpers/ExceptionTracker';
export default {
components: {
@@ -113,7 +113,7 @@ export default {
if (error.name === 'ScriptLoaderError') {
useAlert(this.$t('INBOX_MGMT.DETAILS.ERROR_FB_LOADING'));
} else {
Sentry.captureException(error);
ExceptionTracker.captureException(error);
useAlert(this.$t('INBOX_MGMT.DETAILS.ERROR_FB_AUTH'));
}
}

View File

@@ -5,7 +5,7 @@ import { useAlert } from 'dashboard/composables';
import { loadScript } from 'dashboard/helper/DOMHelpers';
import { buildFacebookLoginScopes } from 'dashboard/helper/facebookScopes';
import * as Sentry from '@sentry/vue';
import ExceptionTracker from 'shared/helpers/ExceptionTracker';
export default {
components: {
@@ -62,7 +62,7 @@ export default {
useAlert(this.$t('INBOX_MGMT.DETAILS.ERROR_FB_LOADING'));
} else {
// if the error was anything else, we capture it and show a toast
Sentry.captureException(error);
ExceptionTracker.captureException(error);
useAlert(this.$t('INBOX_MGMT.DETAILS.ERROR_FB_AUTH'));
}
}

View File

@@ -1,7 +1,7 @@
<script setup>
import { computed } from 'vue';
import Icon from 'next/icon/Icon.vue';
import * as Sentry from '@sentry/vue';
import ExceptionTracker from 'shared/helpers/ExceptionTracker';
import FormSelect from 'v3/components/Form/Select.vue';
const props = defineProps({
@@ -57,7 +57,7 @@ const playAudio = async () => {
audio.src = `/audio/dashboard/${selectedValue.value}.mp3`;
await audio.play();
} catch (error) {
Sentry.captureException(error);
ExceptionTracker.captureException(error);
}
};
</script>

View File

@@ -1,5 +1,5 @@
import types from '../../mutation-types';
import * as Sentry from '@sentry/vue';
import ExceptionTracker from 'shared/helpers/ExceptionTracker';
export const mutations = {
[types.SET_CONTACT_UI_FLAG]($state, data) {
@@ -81,11 +81,11 @@ export const mutations = {
try {
availabilityStatus = data[element.id];
} catch (error) {
Sentry.setContext('contact is undefined', {
ExceptionTracker.setContext('contact is undefined', {
records: $state.records,
data: data,
});
Sentry.captureException(error);
ExceptionTracker.captureException(error);
return;
}

View File

@@ -12,7 +12,7 @@ import {
} from './helpers/actionHelpers';
import messageReadActions from './actions/messageReadActions';
import messageTranslateActions from './actions/messageTranslateActions';
import * as Sentry from '@sentry/vue';
import ExceptionTracker from 'shared/helpers/ExceptionTracker';
import {
handleVoiceCallCreated,
handleVoiceCallUpdated,
@@ -114,10 +114,10 @@ const actions = {
attachments = data.payload;
} catch (error) {
// in case of error, log the error and continue
Sentry.setContext('Conversation', {
ExceptionTracker.setContext('Conversation', {
id: conversationId,
});
Sentry.captureException(error);
ExceptionTracker.captureException(error);
} finally {
// we run the commit even if the request fails
// this ensures that the `attachment` variable is always present on chat

View File

@@ -19,7 +19,6 @@ import { createPinia } from 'pinia';
import router, { initalizeRouter } from 'dashboard/routes';
import store from 'dashboard/store';
import constants from 'dashboard/constants/globals';
import * as Sentry from '@sentry/vue';
import {
initializeAnalyticsEvents,
initializeChatwootEvents,
@@ -49,31 +48,6 @@ app.use(store);
app.use(pinia);
app.use(router);
// [VITE] Disabled this, need to renable later
if (window.errorLoggingConfig) {
Sentry.init({
app,
dsn: window.errorLoggingConfig,
denyUrls: [
// Chrome extensions
/^chrome:\/\//i,
/chrome-extension:/i,
/extensions\//i,
// Locally saved copies
/file:\/\//i,
// Safari extensions.
/safari-web-extension:/i,
/safari-extension:/i,
],
integrations: [Sentry.browserTracingIntegration({ router })],
ignoreErrors: [
'ResizeObserver loop completed with undelivered notifications',
],
});
}
app.use(VueDOMPurifyHTML, domPurifyConfig);
app.use(WootUiKit);
app.use(

View File

@@ -2,7 +2,6 @@ import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';
import i18nMessages from 'dashboard/i18n';
import * as Sentry from '@sentry/vue';
import {
initializeAnalyticsEvents,
initializeChatwootEvents,
@@ -33,30 +32,6 @@ app.use(router);
// Vue.prototype.$emitter = emitter;
app.component('fluent-icon', FluentIcon);
if (window.errorLoggingConfig) {
Sentry.init({
app,
dsn: window.errorLoggingConfig,
denyUrls: [
// Chrome extensions
/^chrome:\/\//i,
/chrome-extension:/i,
/extensions\//i,
// Locally saved copies
/file:\/\//i,
// Safari extensions.
/safari-web-extension:/i,
/safari-extension:/i,
],
integrations: [Sentry.browserTracingIntegration({ router })],
ignoreErrors: [
'ResizeObserver loop completed with undelivered notifications',
],
});
}
initializeChatwootEvents();
initializeAnalyticsEvents();
initalizeRouter();

View File

@@ -0,0 +1,10 @@
// Community Edition exception compatibility surface.
// Intentionally local-only: callers may report a failure without creating
// network traffic or serializing user/account payloads.
const ExceptionTracker = {
setContext() {},
captureException() {},
};
export default ExceptionTracker;
export { ExceptionTracker };

View File

@@ -0,0 +1,19 @@
import ExceptionTracker from '../ExceptionTracker';
describe('ExceptionTracker', () => {
it('accepts context and exceptions without retaining or serializing payloads', () => {
const context = { accountId: 42, token: '[REDACTED]' };
const exception = new Error('local-only failure');
expect(() => {
ExceptionTracker.setContext('operation', context);
ExceptionTracker.captureException(exception);
}).not.toThrow();
expect(Object.keys(ExceptionTracker)).toEqual([
'setContext',
'captureException',
]);
expect(JSON.stringify(ExceptionTracker)).toBe('{}');
});
});

View File

@@ -2,8 +2,6 @@ class Conversations::UnreadCounts::FilteredCountInstrumentation
# Centralizes the rollout-critical filtered unread count signals:
# API response duration, counter duration, snapshot build duration, snapshot state distribution,
# refresh claim rate, build lock acquisition rate, and invalidation/version bump rate.
EVENT_NAME = 'FilteredUnreadCounts'.freeze
METRIC_PREFIX = 'Custom/Conversations/UnreadCounts/Filtered'.freeze
SUMMARY_KEY = :filtered_unread_counts_request_summary
AGGREGATED_INCREMENT_OPERATIONS = %i[snapshot_state refresh_claim build_lock].freeze
SNAPSHOT_STATUSES = %i[fresh stale missing expired].freeze
@@ -65,17 +63,6 @@ class Conversations::UnreadCounts::FilteredCountInstrumentation
def increment(operation, attributes = {})
record_increment_summary(operation, attributes) if aggregated_increment?(operation)
record_event(operation, attributes) unless aggregated_increment?(operation)
record_metric("#{metric_name(operation)}/count", 1)
end
def record_event(operation, attributes = {})
agent = new_relic_agent
return unless agent.respond_to?(:record_custom_event)
agent.record_custom_event(EVENT_NAME, sanitized_attributes(attributes.merge(operation: operation)))
rescue StandardError
nil
end
private
@@ -83,28 +70,12 @@ class Conversations::UnreadCounts::FilteredCountInstrumentation
def record_observation(operation, attributes, started_at, status:)
duration_ms = elapsed_ms_since(started_at)
record_observation_summary(operation, attributes, status: status)
record_metric("#{metric_name(operation)}/duration_ms", duration_ms)
duration_ms
end
def record_request_summary(summary, status, started_at)
duration_ms = elapsed_ms_since(started_at)
summary[:status] = status
summary[:duration_ms] = duration_ms
record_metric("#{metric_name(:api_response)}/duration_ms", duration_ms)
record_event(:request_summary, summary)
end
def record_metric(name, value)
agent = new_relic_agent
return unless agent.respond_to?(:record_metric)
agent.record_metric(name, value)
rescue StandardError
nil
end
def metric_name(operation)
"#{METRIC_PREFIX}/#{operation}"
summary[:duration_ms] = elapsed_ms_since(started_at)
end
def request_summary(account_id)
@@ -160,17 +131,6 @@ class Conversations::UnreadCounts::FilteredCountInstrumentation
current_summary[key] = current_summary.fetch(key, 0) + 1
end
def sanitized_attributes(attributes)
attributes.compact.transform_values do |value|
case value
when String, Integer, Float, TrueClass, FalseClass
value
else
value.to_s
end
end
end
def elapsed_ms_since(started_at)
((monotonic_time - started_at) * 1000).round(2)
end
@@ -178,11 +138,5 @@ class Conversations::UnreadCounts::FilteredCountInstrumentation
def monotonic_time
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
def new_relic_agent
return unless defined?(::NewRelic::Agent)
::NewRelic::Agent
end
end
end

View File

@@ -62,7 +62,6 @@
window.browserConfig = {
browser_name: '<%= browser.name %>',
}
window.errorLoggingConfig = '<%= ENV.fetch('SENTRY_FRONTEND_DSN', '') || ENV.fetch('SENTRY_DSN', '') %>'
</script>
<%= vite_client_tag %>
<%= vite_javascript_tag @application_pack %>