analytics.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  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. /**
  6. * Analytics and metric tracking functionality.
  7. *
  8. * These are primarily driven through hooks provided through the hookstore. For
  9. * sentry.io these are currently mapped to our in-house analytics backend
  10. * 'Reload' and the Amplitude service.
  11. *
  12. * NOTE: sentry.io contributors, you will need to ensure that the eventKey
  13. * passed exists as an event key in the Reload events.py configuration:
  14. *
  15. * https://github.com/getsentry/reload/blob/master/reload_app/events.py
  16. *
  17. * NOTE: sentry.io contributors, if you are using `gauge` or `increment` the
  18. * name must be added to the Reload metrics module:
  19. *
  20. * https://github.com/getsentry/reload/blob/master/reload_app/metrics/__init__.py
  21. */
  22. /**
  23. * This should be with all analytics events regardless of the analytics destination
  24. * which includes Reload, Amplitude, and Google Analytics.
  25. * All events go to Reload. If eventName is defined, events also go to Amplitude.
  26. * For more details, refer to the API defined in hooks.
  27. *
  28. * Should NOT be used directly.
  29. * Instead, use makeAnalyticsFunction to generate an analytics function.
  30. */
  31. export const trackAnalyticsEventV2: Hooks['analytics:track-event-v2'] = (data, options) =>
  32. HookStore.get('analytics:track-event-v2').forEach(cb => cb(data, options));
  33. /**
  34. * @deprecated Use trackAdvancedAnalyticsEvent or another function generated by makeAnalyticsFunction
  35. */
  36. export const trackAnalyticsEvent: Hooks['analytics:track-event'] = options =>
  37. HookStore.get('analytics:track-event').forEach(cb => cb(options));
  38. /**
  39. * @deprecated Use trackAdvancedAnalyticsEvent or another function generated by makeAnalyticsFunction
  40. */
  41. export const trackAdhocEvent: Hooks['analytics:track-adhoc-event'] = options =>
  42. HookStore.get('analytics:track-adhoc-event').forEach(cb => cb(options));
  43. /**
  44. * This should be used to log when a `organization.experiments` experiment
  45. * variant is checked in the application.
  46. *
  47. * Refer for the backend implementation provided through HookStore for more
  48. * details.
  49. */
  50. export const logExperiment: Hooks['analytics:log-experiment'] = options =>
  51. HookStore.get('analytics:log-experiment').forEach(cb => cb(options));
  52. /**
  53. * Legacy analytics tracking.
  54. *
  55. * @deprecated Use trackAdvancedAnalyticsEvent or another function generated by makeAnalyticsFunction
  56. */
  57. export const analytics: Hooks['analytics:event'] = (name, data) =>
  58. HookStore.get('analytics:event').forEach(cb => cb(name, data));
  59. type RecordMetric = Hooks['metrics:event'] & {
  60. endTransaction: (opts: {
  61. /**
  62. * Name of the transaction to end
  63. */
  64. name: string;
  65. }) => void;
  66. mark: (opts: {
  67. /**
  68. * Name of the metric event
  69. */
  70. name: string;
  71. /**
  72. * Additional data that will be sent with measure()
  73. * This is useful if you want to track initial state
  74. */
  75. data?: object;
  76. }) => void;
  77. measure: (opts: {
  78. /**
  79. * Additional data to send with metric event.
  80. * If a key collide with the data in mark(), this will overwrite them
  81. */
  82. data?: object;
  83. /**
  84. * Name of ending mark
  85. */
  86. end?: string;
  87. /**
  88. * Name of the metric event
  89. */
  90. name?: string;
  91. /**
  92. * Do not clean up marks and measurements when completed
  93. */
  94. noCleanup?: boolean;
  95. /**
  96. * Name of starting mark
  97. */
  98. start?: string;
  99. }) => void;
  100. startTransaction: (opts: {
  101. /**
  102. * Name of transaction
  103. */
  104. name: string;
  105. /**
  106. * Optional op code
  107. */
  108. op?: string;
  109. /**
  110. * Optional trace id, defaults to current tx trace
  111. */
  112. traceId?: string;
  113. }) => Transaction;
  114. };
  115. /**
  116. * Used to pass data between metric.mark() and metric.measure()
  117. */
  118. const metricDataStore = new Map<string, object>();
  119. /**
  120. * Record metrics.
  121. */
  122. export const metric: RecordMetric = (name, value, tags) =>
  123. HookStore.get('metrics:event').forEach(cb => cb(name, value, tags));
  124. // JSDOM implements window.performance but not window.performance.mark
  125. const CAN_MARK =
  126. window.performance &&
  127. typeof window.performance.mark === 'function' &&
  128. typeof window.performance.measure === 'function' &&
  129. typeof window.performance.getEntriesByName === 'function' &&
  130. typeof window.performance.clearMeasures === 'function';
  131. metric.mark = function metricMark({name, data = {}}) {
  132. // Just ignore if browser is old enough that it doesn't support this
  133. if (!CAN_MARK) {
  134. return;
  135. }
  136. if (!name) {
  137. throw new Error('Invalid argument provided to `metric.mark`');
  138. }
  139. window.performance.mark(name);
  140. metricDataStore.set(name, data);
  141. };
  142. /**
  143. * Performs a measurement between `start` and `end` (or now if `end` is not
  144. * specified) Calls `metric` with `name` and the measured time difference.
  145. */
  146. metric.measure = function metricMeasure({name, start, end, data = {}, noCleanup} = {}) {
  147. // Just ignore if browser is old enough that it doesn't support this
  148. if (!CAN_MARK) {
  149. return;
  150. }
  151. if (!name || !start) {
  152. throw new Error('Invalid arguments provided to `metric.measure`');
  153. }
  154. let endMarkName = end;
  155. // Can't destructure from performance
  156. const {performance} = window;
  157. // NOTE: Edge REQUIRES an end mark if it is given a start mark
  158. // If we don't have an end mark, create one now.
  159. if (!end) {
  160. endMarkName = `${start}-end`;
  161. performance.mark(endMarkName);
  162. }
  163. // Check if starting mark exists
  164. if (!performance.getEntriesByName(start, 'mark').length) {
  165. return;
  166. }
  167. performance.measure(name, start, endMarkName);
  168. const startData = metricDataStore.get(start) || {};
  169. // Retrieve measurement entries
  170. performance
  171. .getEntriesByName(name, 'measure')
  172. .forEach(measurement =>
  173. metric(measurement.name, measurement.duration, {...startData, ...data})
  174. );
  175. // By default, clean up measurements
  176. if (!noCleanup) {
  177. performance.clearMeasures(name);
  178. performance.clearMarks(start);
  179. performance.clearMarks(endMarkName);
  180. metricDataStore.delete(start);
  181. }
  182. };
  183. /**
  184. * Used to pass data between startTransaction and endTransaction
  185. */
  186. const transactionDataStore = new Map<string, object>();
  187. const getCurrentTransaction = () => {
  188. return Sentry.getCurrentHub().getScope()?.getTransaction();
  189. };
  190. metric.startTransaction = ({name, traceId, op}) => {
  191. if (!traceId) {
  192. traceId = getCurrentTransaction()?.traceId;
  193. }
  194. const transaction = Sentry.startTransaction({name, op, traceId});
  195. transactionDataStore[name] = transaction;
  196. return transaction;
  197. };
  198. metric.endTransaction = ({name}) => {
  199. const transaction = transactionDataStore[name];
  200. if (transaction) {
  201. transaction.finish();
  202. }
  203. };