analytics.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import * as Sentry from '@sentry/react';
  2. import {Transaction} from '@sentry/types';
  3. import HookStore from 'app/stores/hookStore';
  4. import {Hooks} from 'app/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 primarily used for product events. In that case where you
  24. * want to track some one-off Adhoc events, use the `trackAdhocEvent` function.
  25. *
  26. * Generally this is the function you will want to use for event tracking.
  27. *
  28. * Refer for the backend implementation provided through HookStore for more
  29. * details.
  30. */
  31. export const trackAnalyticsEvent: Hooks['analytics:track-event'] = options =>
  32. HookStore.get('analytics:track-event').forEach(cb => cb(options));
  33. /**
  34. * This should be used for adhoc analytics tracking.
  35. *
  36. * This is used for high volume events, and events with unbounded parameters,
  37. * such as tracking search queries.
  38. *
  39. * Refer for the backend implementation provided through HookStore for a more
  40. * thorough explanation of when to use this.
  41. */
  42. export const trackAdhocEvent: Hooks['analytics:track-adhoc-event'] = options =>
  43. HookStore.get('analytics:track-adhoc-event').forEach(cb => cb(options));
  44. /**
  45. * This should be used to log when a `organization.experiments` experiment
  46. * variant is checked in the application.
  47. *
  48. * Refer for the backend implementation provided through HookStore for more
  49. * details.
  50. */
  51. export const logExperiment: Hooks['analytics:log-experiment'] = options =>
  52. HookStore.get('analytics:log-experiment').forEach(cb => cb(options));
  53. /**
  54. * Helper function for `trackAnalyticsEvent` to generically track usage of deprecated features
  55. *
  56. * @param feature A name to identify the feature you are tracking
  57. * @param orgId The organization id
  58. * @param url [optional] The URL
  59. */
  60. export const trackDeprecated = (feature: string, orgId: number, url: string = '') =>
  61. trackAdhocEvent({
  62. eventKey: 'deprecated.feature',
  63. feature,
  64. url,
  65. org_id: orgId && Number(orgId),
  66. });
  67. /**
  68. * Legacy analytics tracking.
  69. *
  70. * @deprecated Prefer `trackAnalyticsEvent` and `trackAdhocEvent`.
  71. */
  72. export const analytics: Hooks['analytics:event'] = (name, data) =>
  73. HookStore.get('analytics:event').forEach(cb => cb(name, data));
  74. type RecordMetric = Hooks['metrics:event'] & {
  75. mark: (opts: {
  76. /**
  77. * Name of the metric event
  78. */
  79. name: string;
  80. /**
  81. * Additional data that will be sent with measure()
  82. * This is useful if you want to track initial state
  83. */
  84. data?: object;
  85. }) => void;
  86. measure: (opts: {
  87. /**
  88. * Name of the metric event
  89. */
  90. name?: string;
  91. /**
  92. * Name of starting mark
  93. */
  94. start?: string;
  95. /**
  96. * Name of ending mark
  97. */
  98. end?: string;
  99. /**
  100. * Additional data to send with metric event.
  101. * If a key collide with the data in mark(), this will overwrite them
  102. */
  103. data?: object;
  104. /**
  105. * Do not clean up marks and measurements when completed
  106. */
  107. noCleanup?: boolean;
  108. }) => void;
  109. startTransaction: (opts: {
  110. /**
  111. * Name of transaction
  112. */
  113. name: string;
  114. /**
  115. * Optional trace id, defaults to current tx trace
  116. */
  117. traceId?: string;
  118. /**
  119. * Optional op code
  120. */
  121. op?: string;
  122. }) => Transaction;
  123. endTransaction: (opts: {
  124. /**
  125. * Name of the transaction to end
  126. */
  127. name: string;
  128. }) => void;
  129. };
  130. /**
  131. * Used to pass data between metric.mark() and metric.measure()
  132. */
  133. const metricDataStore = new Map<string, object>();
  134. /**
  135. * Record metrics.
  136. */
  137. export const metric: RecordMetric = (name, value, tags) =>
  138. HookStore.get('metrics:event').forEach(cb => cb(name, value, tags));
  139. // JSDOM implements window.performance but not window.performance.mark
  140. const CAN_MARK =
  141. window.performance &&
  142. typeof window.performance.mark === 'function' &&
  143. typeof window.performance.measure === 'function' &&
  144. typeof window.performance.getEntriesByName === 'function' &&
  145. typeof window.performance.clearMeasures === 'function';
  146. metric.mark = function metricMark({name, data = {}}) {
  147. // Just ignore if browser is old enough that it doesn't support this
  148. if (!CAN_MARK) {
  149. return;
  150. }
  151. if (!name) {
  152. throw new Error('Invalid argument provided to `metric.mark`');
  153. }
  154. window.performance.mark(name);
  155. metricDataStore.set(name, data);
  156. };
  157. /**
  158. * Performs a measurement between `start` and `end` (or now if `end` is not
  159. * specified) Calls `metric` with `name` and the measured time difference.
  160. */
  161. metric.measure = function metricMeasure({name, start, end, data = {}, noCleanup} = {}) {
  162. // Just ignore if browser is old enough that it doesn't support this
  163. if (!CAN_MARK) {
  164. return;
  165. }
  166. if (!name || !start) {
  167. throw new Error('Invalid arguments provided to `metric.measure`');
  168. }
  169. let endMarkName = end;
  170. // Can't destructure from performance
  171. const {performance} = window;
  172. // NOTE: Edge REQUIRES an end mark if it is given a start mark
  173. // If we don't have an end mark, create one now.
  174. if (!end) {
  175. endMarkName = `${start}-end`;
  176. performance.mark(endMarkName);
  177. }
  178. // Check if starting mark exists
  179. if (!performance.getEntriesByName(start, 'mark').length) {
  180. return;
  181. }
  182. performance.measure(name, start, endMarkName);
  183. const startData = metricDataStore.get(start) || {};
  184. // Retrieve measurement entries
  185. performance
  186. .getEntriesByName(name, 'measure')
  187. .forEach(measurement =>
  188. metric(measurement.name, measurement.duration, {...startData, ...data})
  189. );
  190. // By default, clean up measurements
  191. if (!noCleanup) {
  192. performance.clearMeasures(name);
  193. performance.clearMarks(start);
  194. performance.clearMarks(endMarkName);
  195. metricDataStore.delete(start);
  196. }
  197. };
  198. /**
  199. * Used to pass data between startTransaction and endTransaction
  200. */
  201. const transactionDataStore = new Map<string, object>();
  202. const getCurrentTransaction = () => {
  203. return Sentry.getCurrentHub().getScope()?.getTransaction();
  204. };
  205. metric.startTransaction = ({name, traceId, op}) => {
  206. if (!traceId) {
  207. traceId = getCurrentTransaction()?.traceId;
  208. }
  209. const transaction = Sentry.startTransaction({name, op, traceId});
  210. transactionDataStore[name] = transaction;
  211. return transaction;
  212. };
  213. metric.endTransaction = ({name}) => {
  214. const transaction = transactionDataStore[name];
  215. if (transaction) {
  216. transaction.finish();
  217. }
  218. };