diff --git a/.env.example b/.env.example index d106405e6..d643852ba 100644 --- a/.env.example +++ b/.env.example @@ -210,33 +210,8 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38: # for mobile apps # FCM_SERVER_KEY= -### APM and Error Monitoring configurations -## Elastic APM -## https://www.elastic.co/guide/en/apm/agent/ruby/current/getting-started-rails.html -# ELASTIC_APM_SERVER_URL= -# ELASTIC_APM_SECRET_TOKEN= - -## Sentry -# SENTRY_DSN= - - -## Scout -## https://scoutapm.com/docs/ruby/configuration -# SCOUT_KEY=YOURKEY -# SCOUT_NAME=YOURAPPNAME (Production) -# SCOUT_MONITOR=true - -## NewRelic -# https://docs.newrelic.com/docs/agents/ruby-agent/configuration/ruby-agent-configuration/ -# NEW_RELIC_LICENSE_KEY= -# Set this to true to allow newrelic apm to send logs. -# This is turned off by default. -# NEW_RELIC_APPLICATION_LOGGING_ENABLED= - -## Datadog -## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables -# DD_TRACE_AGENT_URL= - +### Remote observability +# Remote error reporting and APM agents are intentionally disabled in this build. # MaxMindDB API key to download GeoLite2 City database # IP_LOOKUP_API_KEY= diff --git a/Gemfile b/Gemfile index fab536116..5a010cc34 100644 --- a/Gemfile +++ b/Gemfile @@ -124,14 +124,6 @@ gem 'google-cloud-translate-v3', '>= 0.7.0' ##-- apm and error monitoring ---# # loaded only when environment variables are set. # ref application.rb -gem 'datadog', '~> 2.0', require: false -gem 'elastic-apm', require: false -gem 'newrelic_rpm', require: false -gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false -gem 'scout_apm', require: false -gem 'sentry-rails', '>= 5.19.0', require: false -gem 'sentry-ruby', require: false -gem 'sentry-sidekiq', '>= 5.19.0', require: false ##-- background job processing --## gem 'sidekiq', '~> 7.3.10' diff --git a/Gemfile.lock b/Gemfile.lock index 3b37ba82f..540c5b96f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -187,14 +187,6 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - datadog (2.38.0) - cgi - datadog-ruby_core_source (~> 3.5, >= 3.5.3) - libdatadog (~> 36.0.0.1.0) - libddwaf (~> 1.30.0.0.0) - logger - msgpack - datadog-ruby_core_source (3.5.3) date (3.5.1) debug (1.11.1) irb (~> 1.10) @@ -263,10 +255,6 @@ GEM zeitwerk (~> 2.6) ecma-re-validator (0.4.0) regexp_parser (~> 2.2) - elastic-apm (4.6.2) - concurrent-ruby (~> 1.0) - http (>= 3.0) - ruby2_keywords email-provider-info (0.0.1) email_reply_trimmer (0.1.13) erb (6.0.6) @@ -315,9 +303,6 @@ GEM ffi (1.17.2-arm64-darwin) ffi (1.17.2-x86_64-darwin) ffi (1.17.2-x86_64-linux-gnu) - ffi-compiler (1.0.1) - ffi (>= 1.0.0) - rake firecrawl-sdk (1.4.1) flag_shih_tzu (0.3.23) foreman (0.87.2) @@ -418,15 +403,9 @@ GEM logger html2text (0.4.0) nokogiri (>= 1.0, < 2.0) - http (5.1.1) - addressable (~> 2.8) - http-cookie (~> 1.0) - http-form_data (~> 2.2) - llhttp-ffi (~> 0.4.0) http-accept (1.7.0) http-cookie (1.0.5) domain_name (~> 0.5) - http-form_data (2.3.0) httparty (0.24.0) csv mini_mime (>= 1.0.0) @@ -501,26 +480,12 @@ GEM logger (~> 1.6) letter_opener (1.10.0) launchy (>= 2.2, < 4) - libdatadog (36.0.0.1.0) - libdatadog (36.0.0.1.0-arm64-darwin) - libdatadog (36.0.0.1.0-x86_64-linux) - libddwaf (1.30.0.0.2) - ffi (~> 1.0) - libddwaf (1.30.0.0.2-arm64-darwin) - ffi (~> 1.0) - libddwaf (1.30.0.0.2-x86_64-darwin) - ffi (~> 1.0) - libddwaf (1.30.0.0.2-x86_64-linux) - ffi (~> 1.0) line-bot-api (1.28.0) lint_roller (1.1.0) liquid (5.4.0) listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - llhttp-ffi (0.4.0) - ffi-compiler (~> 1.0) - rake (~> 13.0) logger (1.7.0) lograge (0.14.0) actionpack (>= 4) @@ -570,11 +535,6 @@ GEM net-smtp (0.3.4) net-protocol netrc (0.11.0) - newrelic-sidekiq-metrics (1.6.2) - newrelic_rpm (>= 8.0.0) - sidekiq - newrelic_rpm (9.6.0) - base64 nio4r (2.7.5) nokogiri (1.19.4) mini_portile2 (~> 2.8.2) @@ -868,8 +828,6 @@ GEM sprockets (> 3.0) sprockets-rails tilt - scout_apm (5.3.3) - parser scss_lint (0.60.0) sass (~> 3.5, >= 3.5.5) searchkick (5.5.2) @@ -880,15 +838,6 @@ GEM activerecord (>= 4) activesupport (>= 4) selectize-rails (0.12.6) - sentry-rails (5.19.0) - railties (>= 5.0) - sentry-ruby (~> 5.19.0) - sentry-ruby (5.19.0) - bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.19.0) - sentry-ruby (~> 5.19.0) - sidekiq (>= 3.0) sexp_processor (4.17.0) shopify_api (14.8.0) activesupport @@ -1072,7 +1021,6 @@ DEPENDENCIES commonmarker csv-safe database_cleaner - datadog (~> 2.0) debug (~> 1.8) devise (>= 4.9.4) devise-secure_password (= 2.2.1) @@ -1080,7 +1028,6 @@ DEPENDENCIES devise_token_auth (>= 1.2.3) dotenv-rails (>= 3.0.0) down - elastic-apm email-provider-info email_reply_trimmer facebook-messenger @@ -1123,8 +1070,6 @@ DEPENDENCIES mock_redis neighbor net-smtp (~> 0.3.4) - newrelic-sidekiq-metrics (>= 1.6.2) - newrelic_rpm omniauth (>= 2.1.2) omniauth-google-oauth2 (>= 1.1.3) omniauth-oauth2 @@ -1161,13 +1106,9 @@ DEPENDENCIES ruby-openai ruby_llm (>= 1.14.1) ruby_llm-schema - scout_apm scss_lint searchkick seed_dump - sentry-rails (>= 5.19.0) - sentry-ruby - sentry-sidekiq (>= 5.19.0) shopify_api shoulda-matchers sidekiq (~> 7.3.10) diff --git a/app/javascript/dashboard/composables/useTransformKeys.js b/app/javascript/dashboard/composables/useTransformKeys.js index 2397e6319..0cdeec573 100644 --- a/app/javascript/dashboard/composables/useTransformKeys.js +++ b/app/javascript/dashboard/composables/useTransformKeys.js @@ -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; } } diff --git a/app/javascript/dashboard/helper/editorHelper.js b/app/javascript/dashboard/helper/editorHelper.js index e01a2f622..3f35a63fe 100644 --- a/app/javascript/dashboard/helper/editorHelper.js +++ b/app/javascript/dashboard/helper/editorHelper.js @@ -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; diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue index 531797054..e20275673 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue @@ -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')); } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/facebook/Reauthorize.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/facebook/Reauthorize.vue index cf25177ea..52942af41 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/facebook/Reauthorize.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/facebook/Reauthorize.vue @@ -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')); } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertTone.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertTone.vue index c0ed9000a..abc143f36 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertTone.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertTone.vue @@ -1,7 +1,7 @@ diff --git a/app/javascript/dashboard/store/modules/contacts/mutations.js b/app/javascript/dashboard/store/modules/contacts/mutations.js index 5fbd04ed1..3f28f7582 100644 --- a/app/javascript/dashboard/store/modules/contacts/mutations.js +++ b/app/javascript/dashboard/store/modules/contacts/mutations.js @@ -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; } diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js index 9e149e8df..7175af5ce 100644 --- a/app/javascript/dashboard/store/modules/conversations/actions.js +++ b/app/javascript/dashboard/store/modules/conversations/actions.js @@ -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 diff --git a/app/javascript/entrypoints/dashboard.js b/app/javascript/entrypoints/dashboard.js index 8014a6714..d37746b8e 100644 --- a/app/javascript/entrypoints/dashboard.js +++ b/app/javascript/entrypoints/dashboard.js @@ -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( diff --git a/app/javascript/entrypoints/v3app.js b/app/javascript/entrypoints/v3app.js index e69efdb8f..e085320cb 100644 --- a/app/javascript/entrypoints/v3app.js +++ b/app/javascript/entrypoints/v3app.js @@ -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(); diff --git a/app/javascript/shared/helpers/ExceptionTracker.js b/app/javascript/shared/helpers/ExceptionTracker.js new file mode 100644 index 000000000..e43986b78 --- /dev/null +++ b/app/javascript/shared/helpers/ExceptionTracker.js @@ -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 }; diff --git a/app/javascript/shared/helpers/specs/ExceptionTracker.spec.js b/app/javascript/shared/helpers/specs/ExceptionTracker.spec.js new file mode 100644 index 000000000..48bb66c65 --- /dev/null +++ b/app/javascript/shared/helpers/specs/ExceptionTracker.spec.js @@ -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('{}'); + }); +}); diff --git a/app/services/conversations/unread_counts/filtered_count_instrumentation.rb b/app/services/conversations/unread_counts/filtered_count_instrumentation.rb index 707c63f33..9c6f26db1 100644 --- a/app/services/conversations/unread_counts/filtered_count_instrumentation.rb +++ b/app/services/conversations/unread_counts/filtered_count_instrumentation.rb @@ -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 diff --git a/app/views/layouts/vueapp.html.erb b/app/views/layouts/vueapp.html.erb index 0447b6231..d21dbcf09 100644 --- a/app/views/layouts/vueapp.html.erb +++ b/app/views/layouts/vueapp.html.erb @@ -62,7 +62,6 @@ window.browserConfig = { browser_name: '<%= browser.name %>', } - window.errorLoggingConfig = '<%= ENV.fetch('SENTRY_FRONTEND_DSN', '') || ENV.fetch('SENTRY_DSN', '') %>' <%= vite_client_tag %> <%= vite_javascript_tag @application_pack %> diff --git a/config/application.rb b/config/application.rb index 08f0451c1..a5a0bc24c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -8,24 +8,8 @@ require 'rails/all' # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) -## Load the specific APM agent -# We rely on DOTENV to load the environment variables -# We need these environment variables to load the specific APM agent +## Load application dependencies Dotenv::Rails.load -require 'datadog' if ENV.fetch('DD_TRACE_AGENT_URL', false).present? -require 'elastic-apm' if ENV.fetch('ELASTIC_APM_SECRET_TOKEN', false).present? -require 'scout_apm' if ENV.fetch('SCOUT_KEY', false).present? - -if ENV.fetch('NEW_RELIC_LICENSE_KEY', false).present? - require 'newrelic-sidekiq-metrics' - require 'newrelic_rpm' -end - -if ENV.fetch('SENTRY_DSN', false).present? - require 'sentry-ruby' - require 'sentry-rails' - require 'sentry-sidekiq' -end # heroku autoscaling if ENV.fetch('JUDOSCALE_URL', false).present? diff --git a/config/elastic_apm.yml b/config/elastic_apm.yml deleted file mode 100644 index 6f189d3af..000000000 --- a/config/elastic_apm.yml +++ /dev/null @@ -1,5 +0,0 @@ -# Disabled by default to prevent the agent starting up when not required -# To enable, set your ELASTIC_APM_SERVER_URL and ELASTIC_APM_SECRET_TOKEN in your .env -# Additional configuration options can be set below, as per the docs: https://www.elastic.co/guide/en/apm/agent/ruby/current/configuration.html#configuration - -enabled: <%= ENV.fetch('ELASTIC_APM_SECRET_TOKEN', false).present? %> diff --git a/config/initializers/datadog.rb b/config/initializers/datadog.rb deleted file mode 100644 index 79bc7e580..000000000 --- a/config/initializers/datadog.rb +++ /dev/null @@ -1,6 +0,0 @@ -if ENV['DD_TRACE_AGENT_URL'].present? - Datadog.configure do |c| - # Instrumentation - c.tracing.instrument :rails - end -end diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb deleted file mode 100644 index eff36bfc5..000000000 --- a/config/initializers/sentry.rb +++ /dev/null @@ -1,15 +0,0 @@ -if ENV['SENTRY_DSN'].present? - Sentry.init do |config| - config.dsn = ENV['SENTRY_DSN'] - config.enabled_environments = %w[staging production] - - # To activate performance monitoring, set one of these options. - # We recommend adjusting the value in production: - config.traces_sample_rate = 0.1 if ENV['ENABLE_SENTRY_TRANSACTIONS'] - - config.excluded_exceptions += ['Rack::Timeout::RequestTimeoutException', 'MutexApplicationJob::LockAcquisitionError'] - - # to track post data in sentry - config.send_default_pii = true unless ENV['DISABLE_SENTRY_PII'] - end -end diff --git a/config/newrelic.yml b/config/newrelic.yml deleted file mode 100644 index e1482e4a1..000000000 --- a/config/newrelic.yml +++ /dev/null @@ -1,63 +0,0 @@ -# -# This file configures the New Relic Agent. New Relic monitors Ruby, Java, -# .NET, PHP, Python, Node, and Go applications with deep visibility and low -# overhead. For more information, visit www.newrelic.com. -# -# -# For full documentation of agent configuration options, please refer to -# https://docs.newrelic.com/docs/agents/ruby-agent/installation-configuration/ruby-agent-configuration - -common: &default_settings - # Required license key associated with your New Relic account. - license_key: <%= ENV['NEW_RELIC_LICENSE_KEY'] %> - - # Your application name. Renaming here affects where data displays in New - # Relic. For more details, see https://docs.newrelic.com/docs/apm/new-relic-apm/maintenance/renaming-applications - app_name: <%= ENV.fetch('NEW_RELIC_APP_NAME', 'Chatwoot') %> - - distributed_tracing: - enabled: true - - # To disable the agent regardless of other settings, uncomment the following: - agent_enabled: <%= ENV['NEW_RELIC_LICENSE_KEY'].present? && ENV.fetch('NEW_RELIC_AGENT_ENABLED', true) %> - - # Logging level for log/newrelic_agent.log - log_level: <%= ENV.fetch('NEW_RELIC_LOG_LEVEL', 'info') %> - - application_logging: - # If `true`, all logging-related features for the agent can be enabled or disabled - # independently. If `false`, all logging-related features are disabled. - enabled: <%= ENV.fetch('NEW_RELIC_APPLICATION_LOGGING_ENABLED', false) %> - forwarding: - # If `true`, the agent captures log records emitted by this application - enabled: <%= ENV.fetch('NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED', true) == "false" ? false : true %> - # Defines the maximum number of log records to buffer in memory at a time. - max_samples_stored: 30000 - metrics: - # If `true`, the agent captures metrics related to logging for this application. - enabled: true - local_decorating: - # If `true`, the agent decorates logs with metadata to link to entities, hosts, traces, and spans. - # This requires a log forwarder to send your log files to New Relic. - # This should not be used when forwarding is enabled. - enabled: <%= ENV.fetch('NEW_RELIC_APPLICATION_LOGGING_DECORATING_ENABLED', false) %> - - -# Environment-specific settings are in this section. -# RAILS_ENV or RACK_ENV (as appropriate) is used to determine the environment. -# If your application has other named environments, configure them here. -development: - <<: *default_settings - app_name: <%= ENV.fetch('NEW_RELIC_APP_NAME', 'Chatwoot') %> (Development) - -test: - <<: *default_settings - # It doesn't make sense to report to New Relic from automated test runs. - monitor_mode: false - -staging: - <<: *default_settings - app_name: <%= ENV.fetch('NEW_RELIC_APP_NAME', 'Chatwoot') %> (Staging) - -production: - <<: *default_settings diff --git a/config/scout_apm.yml b/config/scout_apm.yml deleted file mode 100644 index 49657597a..000000000 --- a/config/scout_apm.yml +++ /dev/null @@ -1,10 +0,0 @@ -common: &defaults - name: <%= ENV.fetch('SCOUT_NAME', '') %> - key: <%= ENV.fetch('SCOUT_KEY', '') %> - monitor: <%= ENV['SCOUT_KEY'].present? && ENV.fetch('SCOUT_MONITOR', true) %> - -test: - monitor: <%= ENV['SCOUT_KEY'].present? && ENV.fetch('SCOUT_MONITOR', true) %> - -production: - <<: *defaults diff --git a/lib/chatwoot_exception_tracker.rb b/lib/chatwoot_exception_tracker.rb index d2879a08c..38c7d093f 100644 --- a/lib/chatwoot_exception_tracker.rb +++ b/lib/chatwoot_exception_tracker.rb @@ -1,32 +1,23 @@ ############### -# One library to capture_exception and send to the specific service. -# # e as exception, u for user and a for account (user and account are optional) -# Usage: ChatwootExceptionTracker(e, user: u, account: a).capture_exception +# Local-only exception compatibility surface. +# The optional user/account arguments remain accepted for callsite compatibility, +# but are intentionally not retained, serialized, or sent anywhere. ############ class ChatwootExceptionTracker def initialize(exception, user: nil, account: nil) @exception = exception - @user = user - @account = account + discard_optional_context(user) + discard_optional_context(account) end def capture_exception - capture_exception_with_sentry if ENV['SENTRY_DSN'].present? Rails.logger.error @exception end private - def capture_exception_with_sentry - Sentry.with_scope do |scope| - if @account.present? - scope.set_context('account', { id: @account.id, name: @account.name }) - scope.set_tags(account_id: @account.id) - end - - scope.set_user(id: @user.id, email: @user.email) if @user.is_a?(User) - Sentry.capture_exception(@exception) - end + def discard_optional_context(_value) + nil end end diff --git a/lib/tasks/download_report.rake b/lib/tasks/download_report.rake index c68418432..79d240eac 100644 --- a/lib/tasks/download_report.rake +++ b/lib/tasks/download_report.rake @@ -1,9 +1,9 @@ # Download Report Rake Tasks # # Usage: -# POSTGRES_STATEMENT_TIMEOUT=600s NEW_RELIC_AGENT_ENABLED=false bundle exec rake download_report:agent -# POSTGRES_STATEMENT_TIMEOUT=600s NEW_RELIC_AGENT_ENABLED=false bundle exec rake download_report:inbox -# POSTGRES_STATEMENT_TIMEOUT=600s NEW_RELIC_AGENT_ENABLED=false bundle exec rake download_report:label +# POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rake download_report:agent +# POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rake download_report:inbox +# POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rake download_report:label # # The task will prompt for: # - Account ID diff --git a/package.json b/package.json index 22687f037..9434e02c2 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "@rails/actioncable": "6.1.3", "@rails/ujs": "^7.1.400", "@scmmishra/pico-search": "0.6.0", - "@sentry/vue": "^8.55.0", "@sindresorhus/slugify": "2.2.1", "@tailwindcss/typography": "^0.5.19", "@tanstack/vue-table": "^8.20.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b1147af3..9d5febb8a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,9 +63,6 @@ importers: '@scmmishra/pico-search': specifier: 0.6.0 version: 0.6.0 - '@sentry/vue': - specifier: ^8.55.0 - version: 8.55.0(pinia@3.0.4(typescript@5.6.2)(vue@3.5.12(typescript@5.6.2)))(vue@3.5.12(typescript@5.6.2)) '@sindresorhus/slugify': specifier: 2.2.1 version: 2.2.1 @@ -1264,40 +1261,6 @@ packages: '@scmmishra/pico-search@0.6.0': resolution: {integrity: sha512-1zC2cAwPWuv38VEh0It90fdUWkvX75OwBUjgTj+d5LTltARnf3ydbpcN2Ucl0aATBMmaNqPMcVvT25IOCAqCEA==} - '@sentry-internal/browser-utils@8.55.0': - resolution: {integrity: sha512-ROgqtQfpH/82AQIpESPqPQe0UyWywKJsmVIqi3c5Fh+zkds5LUxnssTj3yNd1x+kxaPDVB023jAP+3ibNgeNDw==} - engines: {node: '>=14.18'} - - '@sentry-internal/feedback@8.55.0': - resolution: {integrity: sha512-cP3BD/Q6pquVQ+YL+rwCnorKuTXiS9KXW8HNKu4nmmBAyf7urjs+F6Hr1k9MXP5yQ8W3yK7jRWd09Yu6DHWOiw==} - engines: {node: '>=14.18'} - - '@sentry-internal/replay-canvas@8.55.0': - resolution: {integrity: sha512-nIkfgRWk1091zHdu4NbocQsxZF1rv1f7bbp3tTIlZYbrH62XVZosx5iHAuZG0Zc48AETLE7K4AX9VGjvQj8i9w==} - engines: {node: '>=14.18'} - - '@sentry-internal/replay@8.55.0': - resolution: {integrity: sha512-roCDEGkORwolxBn8xAKedybY+Jlefq3xYmgN2fr3BTnsXjSYOPC7D1/mYqINBat99nDtvgFvNfRcZPiwwZ1hSw==} - engines: {node: '>=14.18'} - - '@sentry/browser@8.55.0': - resolution: {integrity: sha512-1A31mCEWCjaMxJt6qGUK+aDnLDcK6AwLAZnqpSchNysGni1pSn1RWSmk9TBF8qyTds5FH8B31H480uxMPUJ7Cw==} - engines: {node: '>=14.18'} - - '@sentry/core@8.55.0': - resolution: {integrity: sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==} - engines: {node: '>=14.18'} - - '@sentry/vue@8.55.0': - resolution: {integrity: sha512-J6lcpzL39snV/spoGpwyk5Rp1wSFxOV4qV1NhQ9OlLHORVBp/Xpw7cEA0oKqG2w1wVtCV+gC5Jjf9HTmYiHQOQ==} - engines: {node: '>=14.18'} - peerDependencies: - pinia: 2.x || 3.x - vue: 2.x || 3.x - peerDependenciesMeta: - pinia: - optional: true - '@sindresorhus/slugify@2.2.1': resolution: {integrity: sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==} engines: {node: '>=12'} @@ -4972,7 +4935,6 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.8 @@ -5860,42 +5822,6 @@ snapshots: '@scmmishra/pico-search@0.6.0': {} - '@sentry-internal/browser-utils@8.55.0': - dependencies: - '@sentry/core': 8.55.0 - - '@sentry-internal/feedback@8.55.0': - dependencies: - '@sentry/core': 8.55.0 - - '@sentry-internal/replay-canvas@8.55.0': - dependencies: - '@sentry-internal/replay': 8.55.0 - '@sentry/core': 8.55.0 - - '@sentry-internal/replay@8.55.0': - dependencies: - '@sentry-internal/browser-utils': 8.55.0 - '@sentry/core': 8.55.0 - - '@sentry/browser@8.55.0': - dependencies: - '@sentry-internal/browser-utils': 8.55.0 - '@sentry-internal/feedback': 8.55.0 - '@sentry-internal/replay': 8.55.0 - '@sentry-internal/replay-canvas': 8.55.0 - '@sentry/core': 8.55.0 - - '@sentry/core@8.55.0': {} - - '@sentry/vue@8.55.0(pinia@3.0.4(typescript@5.6.2)(vue@3.5.12(typescript@5.6.2)))(vue@3.5.12(typescript@5.6.2))': - dependencies: - '@sentry/browser': 8.55.0 - '@sentry/core': 8.55.0 - vue: 3.5.12(typescript@5.6.2) - optionalDependencies: - pinia: 3.0.4(typescript@5.6.2)(vue@3.5.12(typescript@5.6.2)) - '@sindresorhus/slugify@2.2.1': dependencies: '@sindresorhus/transliterate': 1.6.0 @@ -5963,7 +5889,6 @@ snapshots: '@types/web-bluetooth@0.0.20': {} - '@ungap/structured-clone@1.2.0': {} '@videojs/http-streaming@2.15.1(video.js@7.21.1)': @@ -9884,7 +9809,6 @@ snapshots: wavesurfer.js@7.8.6: {} - webidl-conversions@7.0.0: {} webidl-conversions@8.0.0: {} diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb index dec75fc46..534dfc358 100644 --- a/spec/controllers/dashboard_controller_spec.rb +++ b/spec/controllers/dashboard_controller_spec.rb @@ -6,18 +6,28 @@ describe '/app/login', type: :request do get '/app/login' expect(response).to have_http_status(:success) end + it 'does not serialize the cloud analytics token' do allow(GlobalConfig).to receive(:get).and_return( - { 'CLOUD_ANALYTICS_TOKEN' => 'analytics-test-token' } + { 'CLOUD_ANALYTICS_TOKEN' => '[REDACTED]' } ) get '/app/login' expect(response).to have_http_status(:success) expect(response.body).not_to include('analyticsConfig') - expect(response.body).not_to include('analytics-test-token') + expect(response.body).not_to include('[REDACTED]') end + it 'does not serialize the frontend error reporting DSN' do + with_modified_env SENTRY_FRONTEND_DSN: 'https://sentry.invalid/frontend', SENTRY_DSN: 'https://sentry.invalid/backend' do + get '/app/login' + + expect(response).to have_http_status(:success) + expect(response.body).not_to include('errorLoggingConfig') + expect(response.body).not_to include('sentry.invalid') + end + end end context 'with DEFAULT_LOCALE' do diff --git a/spec/lib/chatwoot_exception_tracker_spec.rb b/spec/lib/chatwoot_exception_tracker_spec.rb index 317c69b34..0cc69637b 100644 --- a/spec/lib/chatwoot_exception_tracker_spec.rb +++ b/spec/lib/chatwoot_exception_tracker_spec.rb @@ -1,26 +1,22 @@ require 'rails_helper' -# explicitly requiring since we are loading apms conditionally in application.rb -require 'sentry-ruby' describe ChatwootExceptionTracker do - it 'use rails logger if no tracker is configured' do - expect(Rails.logger).to receive(:error).with('random') - described_class.new('random').capture_exception + it 'writes the exception to the Rails logger' do + exception = StandardError.new('local failure') + + expect(Rails.logger).to receive(:error).with(exception) + + described_class.new(exception).capture_exception end - context 'with sentry DSN' do - before do - # since sentry is not initated in test, we need to do it manually - Sentry.init do |config| - config.dsn = 'test' - end - end + it 'stays local when a Sentry DSN is present' do + exception = StandardError.new('private failure') + user = build(:user, email: 'person@example.invalid', name: 'Private User') + account = build(:account, name: 'Private Account') - it 'will call sentry capture exception' do - with_modified_env SENTRY_DSN: 'random dsn' do - expect(Sentry).to receive(:capture_exception).with('random') - described_class.new('random').capture_exception - end + with_modified_env SENTRY_DSN: 'https://sentry.invalid/project' do + expect(Rails.logger).to receive(:error).with(exception) + described_class.new(exception, user: user, account: account).capture_exception end end end diff --git a/spec/services/conversations/unread_counts/filtered_count_instrumentation_spec.rb b/spec/services/conversations/unread_counts/filtered_count_instrumentation_spec.rb index 4fb6c6d36..78aab09dd 100644 --- a/spec/services/conversations/unread_counts/filtered_count_instrumentation_spec.rb +++ b/spec/services/conversations/unread_counts/filtered_count_instrumentation_spec.rb @@ -1,119 +1,40 @@ require 'rails_helper' RSpec.describe Conversations::UnreadCounts::FilteredCountInstrumentation do - let(:new_relic_agent) do - Class.new do - def self.record_custom_event(*) end - def self.record_metric(*) end - end - end - - before do - stub_const('NewRelic::Agent', new_relic_agent) - allow(new_relic_agent).to receive(:record_custom_event) - allow(new_relic_agent).to receive(:record_metric) - end - describe '.observe' do - it 'records duration metrics without custom events around successful operations' do + it 'returns the operation result without remote instrumentation' do result = described_class.observe(:counter_perform, account_id: 1, snapshot_scope: :built_in_filter) { 'ok' } expect(result).to eq('ok') - expect(new_relic_agent).not_to have_received(:record_custom_event) - expect(new_relic_agent).to have_received(:record_metric).with( - 'Custom/Conversations/UnreadCounts/Filtered/counter_perform/duration_ms', - kind_of(Float) - ) end - it 'records failed operations and re-raises the original error' do + it 're-raises failed operations without remote instrumentation' do error = StandardError.new('boom') expect do described_class.observe(:snapshot_build, account_id: 1) { raise error } end.to raise_error(error) - expect(new_relic_agent).not_to have_received(:record_custom_event) - expect(new_relic_agent).to have_received(:record_metric).with( - 'Custom/Conversations/UnreadCounts/Filtered/snapshot_build/duration_ms', - kind_of(Float) - ) end end describe '.increment' do - it 'records count metrics without custom events for aggregated read-path operations' do - described_class.increment(:snapshot_state, account_id: 1, snapshot_status: :fresh) - - expect(new_relic_agent).not_to have_received(:record_custom_event) - expect(new_relic_agent).to have_received(:record_metric).with( - 'Custom/Conversations/UnreadCounts/Filtered/snapshot_state/count', - 1 - ) - end - - it 'keeps custom events for invalidation signals' do - described_class.increment(:invalidation, account_id: 1, invalidation_scope: :conversation) - - expect(new_relic_agent).to have_received(:record_custom_event).with( - 'FilteredUnreadCounts', - hash_including( - account_id: 1, - invalidation_scope: 'conversation', - operation: 'invalidation' - ) - ) - expect(new_relic_agent).to have_received(:record_metric).with( - 'Custom/Conversations/UnreadCounts/Filtered/invalidation/count', - 1 - ) - end - - it 'does not raise when New Relic is unavailable' do - allow(described_class).to receive(:new_relic_agent).and_return(nil) - - expect { described_class.increment(:snapshot_state, account_id: 1) }.not_to raise_error + it 'keeps local summary counters without requiring an APM agent' do + expect do + described_class.increment(:snapshot_state, account_id: 1, snapshot_status: :fresh) + described_class.increment(:invalidation, account_id: 1, invalidation_scope: :conversation) + end.not_to raise_error end end describe '.summarize_request' do - it 'records one custom event with aggregated request counters' do + it 'returns the wrapped result while keeping request summaries local' do result = described_class.summarize_request(account_id: 1) do - described_class.increment(:snapshot_state, account_id: 1, snapshot_scope: :built_in_filter, snapshot_status: :fresh) described_class.increment(:snapshot_state, account_id: 1, snapshot_scope: :filter, snapshot_status: :missing) - described_class.increment(:refresh_claim, account_id: 1, snapshot_scope: :filter, claimed: true) - described_class.increment(:refresh_claim, account_id: 1, snapshot_scope: :filter, claimed: false) - described_class.increment(:build_lock, account_id: 1, snapshot_scope: :filter, acquired: true) described_class.observe(:snapshot_build, account_id: 1, snapshot_scope: :filter) { 'built' } - 'ok' end expect(result).to eq('ok') - expect(new_relic_agent).to have_received(:record_custom_event).once.with( - 'FilteredUnreadCounts', - hash_including( - account_id: 1, - build_lock_acquired_count: 1, - duration_ms: kind_of(Float), - filter_build_lock_acquired_count: 1, - filter_refresh_claimed_count: 1, - filter_refresh_skipped_count: 1, - filter_snapshot_build_success_count: 1, - filter_snapshot_count: 1, - operation: 'request_summary', - refresh_claimed_count: 1, - refresh_skipped_count: 1, - snapshot_build_success_count: 1, - snapshot_fresh_count: 1, - snapshot_missing_count: 1, - snapshot_total_count: 2, - status: 'success' - ) - ) - expect(new_relic_agent).to have_received(:record_metric).with( - 'Custom/Conversations/UnreadCounts/Filtered/api_response/duration_ms', - kind_of(Float) - ) end it 'records summary errors and re-raises the original error' do @@ -122,16 +43,6 @@ RSpec.describe Conversations::UnreadCounts::FilteredCountInstrumentation do expect do described_class.summarize_request(account_id: 1) { raise error } end.to raise_error(error) - - expect(new_relic_agent).to have_received(:record_custom_event).with( - 'FilteredUnreadCounts', - hash_including( - account_id: 1, - error_class: 'StandardError', - operation: 'request_summary', - status: 'error' - ) - ) end end end