analytics.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. import * as Sentry from '@sentry/react';
  2. import {Transaction} from '@sentry/types';
  3. import HookStore from 'sentry/stores/hookStore';
  4. import {Hooks} from 'sentry/types/hooks';
  5. import {
  6. aiSuggestedSolutionEventMap,
  7. AiSuggestedSolutionEventParameters,
  8. } from './analytics/aiSuggestedSolutionAnalyticsEvents';
  9. import {coreUIEventMap, CoreUIEventParameters} from './analytics/coreuiAnalyticsEvents';
  10. import {
  11. dashboardsEventMap,
  12. DashboardsEventParameters,
  13. } from './analytics/dashboardsAnalyticsEvents';
  14. import {
  15. discoverEventMap,
  16. DiscoverEventParameters,
  17. } from './analytics/discoverAnalyticsEvents';
  18. import {
  19. dynamicSamplingEventMap,
  20. DynamicSamplingEventParameters,
  21. } from './analytics/dynamicSamplingAnalyticsEvents';
  22. import {
  23. ecosystemEventMap,
  24. EcosystemEventParameters,
  25. } from './analytics/ecosystemAnalyticsEvents';
  26. import {growthEventMap, GrowthEventParameters} from './analytics/growthAnalyticsEvents';
  27. import {integrationEventMap, IntegrationEventParameters} from './analytics/integrations';
  28. import {issueEventMap, IssueEventParameters} from './analytics/issueAnalyticsEvents';
  29. import makeAnalyticsFunction from './analytics/makeAnalyticsFunction';
  30. import {
  31. monitorsEventMap,
  32. MonitorsEventParameters,
  33. } from './analytics/monitorsAnalyticsEvents';
  34. import {
  35. onboardingEventMap,
  36. OnboardingEventParameters,
  37. } from './analytics/onboardingAnalyticsEvents';
  38. import {
  39. performanceEventMap,
  40. PerformanceEventParameters,
  41. } from './analytics/performanceAnalyticsEvents';
  42. import {
  43. profilingEventMap,
  44. ProfilingEventParameters,
  45. } from './analytics/profilingAnalyticsEvents';
  46. import {
  47. projectCreationEventMap,
  48. ProjectCreationEventParameters,
  49. } from './analytics/projectCreationAnalyticsEvents';
  50. import {
  51. releasesEventMap,
  52. ReleasesEventParameters,
  53. } from './analytics/releasesAnalyticsEvents';
  54. import {replayEventMap, ReplayEventParameters} from './analytics/replayAnalyticsEvents';
  55. import {searchEventMap, SearchEventParameters} from './analytics/searchAnalyticsEvents';
  56. import {
  57. settingsEventMap,
  58. SettingsEventParameters,
  59. } from './analytics/settingsAnalyticsEvents';
  60. import {
  61. stackTraceEventMap,
  62. StackTraceEventParameters,
  63. } from './analytics/stackTraceAnalyticsEvents';
  64. import {
  65. TeamInsightsEventParameters,
  66. workflowEventMap,
  67. } from './analytics/workflowAnalyticsEvents';
  68. interface EventParameters
  69. extends GrowthEventParameters,
  70. CoreUIEventParameters,
  71. DashboardsEventParameters,
  72. DiscoverEventParameters,
  73. IssueEventParameters,
  74. MonitorsEventParameters,
  75. PerformanceEventParameters,
  76. ProfilingEventParameters,
  77. ReleasesEventParameters,
  78. ReplayEventParameters,
  79. SearchEventParameters,
  80. SettingsEventParameters,
  81. TeamInsightsEventParameters,
  82. DynamicSamplingEventParameters,
  83. OnboardingEventParameters,
  84. StackTraceEventParameters,
  85. AiSuggestedSolutionEventParameters,
  86. EcosystemEventParameters,
  87. IntegrationEventParameters,
  88. ProjectCreationEventParameters,
  89. Record<string, Record<string, any>> {}
  90. const allEventMap: Record<string, string | null> = {
  91. ...coreUIEventMap,
  92. ...dashboardsEventMap,
  93. ...discoverEventMap,
  94. ...growthEventMap,
  95. ...issueEventMap,
  96. ...monitorsEventMap,
  97. ...performanceEventMap,
  98. ...profilingEventMap,
  99. ...releasesEventMap,
  100. ...replayEventMap,
  101. ...searchEventMap,
  102. ...settingsEventMap,
  103. ...workflowEventMap,
  104. ...dynamicSamplingEventMap,
  105. ...onboardingEventMap,
  106. ...stackTraceEventMap,
  107. ...aiSuggestedSolutionEventMap,
  108. ...ecosystemEventMap,
  109. ...integrationEventMap,
  110. ...projectCreationEventMap,
  111. };
  112. /**
  113. * This should be with all analytics events regardless of the analytics destination
  114. * which includes Reload, Amplitude, and Google Analytics.
  115. * All events go to Reload. If eventName is defined, events also go to Amplitude.
  116. * For more details, refer to makeAnalyticsFunction.
  117. *
  118. * Should be used for all analytics that are defined in Sentry.
  119. */
  120. export const trackAnalytics = makeAnalyticsFunction<EventParameters>(allEventMap);
  121. /**
  122. * Analytics and metric tracking functionality.
  123. *
  124. * These are primarily driven through hooks provided through the hookstore. For
  125. * sentry.io these are currently mapped to our in-house analytics backend
  126. * 'Reload' and the Amplitude service.
  127. *
  128. * NOTE: sentry.io contributors, you will need to ensure that the eventKey
  129. * passed exists as an event key in the Reload events.py configuration:
  130. *
  131. * https://github.com/getsentry/reload/blob/master/reload_app/events.py
  132. *
  133. * NOTE: sentry.io contributors, if you are using `gauge` or `increment` the
  134. * name must be added to the Reload metrics module:
  135. *
  136. * https://github.com/getsentry/reload/blob/master/reload_app/metrics/__init__.py
  137. */
  138. /**
  139. * This should be with all analytics events regardless of the analytics destination
  140. * which includes Reload, Amplitude, and Google Analytics.
  141. * All events go to Reload. If eventName is defined, events also go to Amplitude.
  142. * For more details, refer to the API defined in hooks.
  143. *
  144. * Should NOT be used directly.
  145. * Instead, use makeAnalyticsFunction to generate an analytics function.
  146. */
  147. export const rawTrackAnalyticsEvent: Hooks['analytics:raw-track-event'] = (
  148. data,
  149. options
  150. ) => HookStore.get('analytics:raw-track-event').forEach(cb => cb(data, options));
  151. /**
  152. * This should be used to log when a `organization.experiments` experiment
  153. * variant is checked in the application.
  154. *
  155. * Refer for the backend implementation provided through HookStore for more
  156. * details.
  157. */
  158. export const logExperiment: Hooks['analytics:log-experiment'] = options =>
  159. HookStore.get('analytics:log-experiment').forEach(cb => cb(options));
  160. type RecordMetric = Hooks['metrics:event'] & {
  161. endTransaction: (opts: {
  162. /**
  163. * Name of the transaction to end
  164. */
  165. name: string;
  166. }) => void;
  167. mark: (opts: {
  168. /**
  169. * Name of the metric event
  170. */
  171. name: string;
  172. /**
  173. * Additional data that will be sent with measure()
  174. * This is useful if you want to track initial state
  175. */
  176. data?: object;
  177. }) => void;
  178. measure: (opts: {
  179. /**
  180. * Additional data to send with metric event.
  181. * If a key collide with the data in mark(), this will overwrite them
  182. */
  183. data?: object;
  184. /**
  185. * Name of ending mark
  186. */
  187. end?: string;
  188. /**
  189. * Name of the metric event
  190. */
  191. name?: string;
  192. /**
  193. * Do not clean up marks and measurements when completed
  194. */
  195. noCleanup?: boolean;
  196. /**
  197. * Name of starting mark
  198. */
  199. start?: string;
  200. }) => void;
  201. startTransaction: (opts: {
  202. /**
  203. * Name of transaction
  204. */
  205. name: string;
  206. /**
  207. * Optional op code
  208. */
  209. op?: string;
  210. /**
  211. * Optional trace id, defaults to current tx trace
  212. */
  213. traceId?: string;
  214. }) => Transaction;
  215. };
  216. /**
  217. * Used to pass data between metric.mark() and metric.measure()
  218. */
  219. const metricDataStore = new Map<string, object>();
  220. /**
  221. * Record metrics.
  222. */
  223. export const metric: RecordMetric = (name, value, tags) =>
  224. HookStore.get('metrics:event').forEach(cb => cb(name, value, tags));
  225. // JSDOM implements window.performance but not window.performance.mark
  226. const CAN_MARK =
  227. window.performance &&
  228. typeof window.performance.mark === 'function' &&
  229. typeof window.performance.measure === 'function' &&
  230. typeof window.performance.getEntriesByName === 'function' &&
  231. typeof window.performance.clearMeasures === 'function';
  232. metric.mark = function metricMark({name, data = {}}) {
  233. // Just ignore if browser is old enough that it doesn't support this
  234. if (!CAN_MARK) {
  235. return;
  236. }
  237. if (!name) {
  238. throw new Error('Invalid argument provided to `metric.mark`');
  239. }
  240. window.performance.mark(name);
  241. metricDataStore.set(name, data);
  242. };
  243. /**
  244. * Performs a measurement between `start` and `end` (or now if `end` is not
  245. * specified) Calls `metric` with `name` and the measured time difference.
  246. */
  247. metric.measure = function metricMeasure({name, start, end, data = {}, noCleanup} = {}) {
  248. // Just ignore if browser is old enough that it doesn't support this
  249. if (!CAN_MARK) {
  250. return;
  251. }
  252. if (!name || !start) {
  253. throw new Error('Invalid arguments provided to `metric.measure`');
  254. }
  255. let endMarkName = end;
  256. // Can't destructure from performance
  257. const {performance} = window;
  258. // NOTE: Edge REQUIRES an end mark if it is given a start mark
  259. // If we don't have an end mark, create one now.
  260. if (!end) {
  261. endMarkName = `${start}-end`;
  262. performance.mark(endMarkName);
  263. }
  264. // Check if starting mark exists
  265. if (!performance.getEntriesByName(start, 'mark').length) {
  266. return;
  267. }
  268. performance.measure(name, start, endMarkName);
  269. const startData = metricDataStore.get(start) || {};
  270. // Retrieve measurement entries
  271. performance
  272. .getEntriesByName(name, 'measure')
  273. .forEach(measurement =>
  274. metric(measurement.name, measurement.duration, {...startData, ...data})
  275. );
  276. // By default, clean up measurements
  277. if (!noCleanup) {
  278. performance.clearMeasures(name);
  279. performance.clearMarks(start);
  280. performance.clearMarks(endMarkName);
  281. metricDataStore.delete(start);
  282. }
  283. };
  284. /**
  285. * Used to pass data between startTransaction and endTransaction
  286. */
  287. const transactionDataStore = new Map<string, object>();
  288. const getCurrentTransaction = () => {
  289. return Sentry.getCurrentHub().getScope()?.getTransaction();
  290. };
  291. metric.startTransaction = ({name, traceId, op}) => {
  292. if (!traceId) {
  293. traceId = getCurrentTransaction()?.traceId;
  294. }
  295. const transaction = Sentry.startTransaction({name, op, traceId});
  296. transactionDataStore[name] = transaction;
  297. return transaction;
  298. };
  299. metric.endTransaction = ({name}) => {
  300. const transaction = transactionDataStore[name];
  301. if (transaction) {
  302. transaction.finish();
  303. }
  304. };