[privacy] make observability local-only
This commit is contained in:
29
.env.example
29
.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=
|
||||
|
||||
8
Gemfile
8
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'
|
||||
|
||||
59
Gemfile.lock
59
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)
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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? %>
|
||||
@@ -1,6 +0,0 @@
|
||||
if ENV['DD_TRACE_AGENT_URL'].present?
|
||||
Datadog.configure do |c|
|
||||
# Instrumentation
|
||||
c.tracing.instrument :rails
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
76
pnpm-lock.yaml
generated
76
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
it 'keeps local summary counters without requiring an APM agent' do
|
||||
expect 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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user