[privacy] make observability local-only
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
10
app/javascript/shared/helpers/ExceptionTracker.js
Normal file
10
app/javascript/shared/helpers/ExceptionTracker.js
Normal 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 };
|
||||
19
app/javascript/shared/helpers/specs/ExceptionTracker.spec.js
Normal file
19
app/javascript/shared/helpers/specs/ExceptionTracker.spec.js
Normal 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('{}');
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
Reference in New Issue
Block a user