123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- import * as qs from 'query-string';
- import ConfigStore from 'sentry/stores/configStore';
- import type {Hooks} from 'sentry/types/hooks';
- import type {Organization} from 'sentry/types/organization';
- import type {User} from 'sentry/types/user';
- import getDaysSinceDate from 'sentry/utils/getDaysSinceDate';
- import {uniqueId} from 'sentry/utils/guid';
- import localStorage from 'sentry/utils/localStorage';
- import sessionStorage from 'sentry/utils/sessionStorage';
- import type {Subscription} from 'getsentry/types';
- import trackAmplitudeEvent from './trackAmplitudeEvent';
- import trackMarketingEvent from './trackMarketingEvent';
- import trackPendoEvent from './trackPendoEvent';
- import trackReloadEvent from './trackReloadEvent';
- /**
- * Fields that are listed here which are passed to trackAnalyticsEvent's data
- * will automatically be coerced into integers.
- */
- const COERCE_FIELDS = ['project_id', 'organization_id', 'user_id', 'org_id'];
- function coerceNumber(value: string | undefined | null) {
- const originalValue = value;
- if (value === undefined) {
- return undefined;
- }
- if (value === null) {
- return null;
- }
- // Attempt to coerce the value to a number
- const numberValue = Number(value);
- // Still unable to coerce to a number? that's a failure
- if (isNaN(numberValue)) {
- throw new Error(`Unable to coerce value to number: '${originalValue}'`);
- }
- return numberValue;
- }
- const MARKETING_EVENT_NAMES = new Set([
- 'Growth: Onboarding Load Choose Platform Page',
- 'Growth: Onboarding Choose Platform',
- 'Growth: Onboarding Start Onboarding',
- 'Growth: Onboarding View Sample Event',
- 'Growth: Onboarding Click Set Up Your Project',
- 'Growth: Onboarding Take to Error',
- 'Growth: Onboarding Clicked Need Help',
- ]);
- const ANALYTICS_SESSION = 'ANALYTICS_SESSION';
- const startAnalyticsSession = () => {
- const sessionId = uniqueId();
- sessionStorage.setItem(ANALYTICS_SESSION, sessionId);
- return sessionId;
- };
- const getAnalyticsSessionId = () => sessionStorage.getItem(ANALYTICS_SESSION);
- const hasAnalyticsDebug = () => localStorage.getItem('DEBUG_ANALYTICS_GETSENTRY') === '1';
- const getCustomReferrer = () => {
- try {
- // pull the referrer from the query parameter of the page
- const {referrer} = qs.parse(window.location.search) || {};
- if (referrer && typeof referrer === 'string') {
- return referrer;
- }
- } catch {
- // ignore if this fails to parse
- // this can happen if we have an invalid query string
- // e.g. unencoded "%"
- }
- return undefined;
- };
- const getOrganizationId = (
- organization: Organization | string | null
- ): number | undefined | null => {
- // this should never happen but there are components that use withOrganization
- // that might end up with an undefined org if used incorrectly
- if (organization === undefined) {
- // eslint-disable-next-line no-console
- console.warn('Unexpected undefined organization');
- return undefined;
- }
- if (typeof organization === 'string') {
- const orgId = Number(organization);
- if (isNaN(orgId)) {
- // eslint-disable-next-line no-console
- console.warn(`Invalid organization ID: ${organization}`);
- return undefined;
- }
- return orgId;
- }
- // if organization is null, organization_id needs to be null
- return organization === null ? null : Number(organization.id);
- };
- const getOrganizationAge = (
- organization: Organization | string | null
- ): number | null => {
- if (typeof organization === 'string') {
- return null;
- }
- if (typeof organization?.dateCreated === 'string') {
- const orgAge = getDaysSinceDate(organization?.dateCreated);
- return orgAge;
- }
- return null;
- };
- const getUserAge = (user: User): number => {
- return getDaysSinceDate(user.dateJoined);
- };
- type RawTrackEventHook = Hooks['analytics:raw-track-event'];
- type Params = Parameters<RawTrackEventHook>[0] & {
- subscription?: Subscription;
- };
- type Options = Parameters<RawTrackEventHook>[1];
- /**
- * Returns true if the organization input has all the properties of a full organization
- */
- function isFullOrganization(
- organization: Params['organization']
- ): organization is Organization {
- return !!organization && typeof organization !== 'string';
- }
- export default function rawTrackAnalyticsEvent(
- {eventKey, eventName, organization, subscription, ...data}: Params,
- options?: Options
- ) {
- try {
- // apply custom function map parameters
- const {mapValuesFn} = options || {};
- if (mapValuesFn) {
- data = mapValuesFn(data);
- }
- const time = options?.time;
- const organization_id = getOrganizationId(organization);
- // Coerce number fields
- Object.keys(data)
- .filter(field => COERCE_FIELDS.includes(field))
- .forEach(field => (data[field] = coerceNumber(data[field])));
- let sessionId = options?.startSession
- ? startAnalyticsSession()
- : getAnalyticsSessionId();
- // we should always have a session id but if we don't, we should generate one
- if (!sessionId) {
- sessionId = startAnalyticsSession();
- }
- data.analytics_session_id = sessionId;
- // add custom referrer if available
- const customReferrer = getCustomReferrer();
- if (customReferrer) {
- data.custom_referrer = customReferrer;
- }
- // add in previous referrer if different than custom referrer
- const prevReferrer = sessionStorage.getItem('previous_referrer');
- if (prevReferrer && prevReferrer !== customReferrer) {
- data.previous_referrer = prevReferrer;
- }
- // pass in properties if we have the full organization
- if (isFullOrganization(organization)) {
- data.role = organization.orgRole;
- }
- // add in plan information
- if (subscription) {
- data.plan = data.plan || subscription.plan;
- if (data.can_trial === undefined) {
- data.can_trial = subscription.canTrial;
- }
- if (data.is_trial === undefined) {
- data.is_trial = subscription.isTrial;
- }
- // we can add more fields but we should be carefull about which ones to add
- // since Amplitude is an external vendor
- }
- // debug mode will console.log the event parameters
- if (hasAnalyticsDebug()) {
- // eslint-disable-next-line no-console
- console.log('rawTrackAnalyticsEvent', {eventKey, eventName, ...data});
- }
- // Prepare reloads data payload. If the organization_id is passed we include
- // that in the data payload.
- const user = ConfigStore.get('user');
- const reloadData = {
- user_id: coerceNumber(user?.id),
- org_id: organization_id,
- allow_no_schema: true,
- sent_at: (time || Date.now()).toString(),
- ...data,
- };
- trackReloadEvent(eventKey, reloadData);
- if (eventName && organization_id !== undefined) {
- const orgAge = getOrganizationAge(organization);
- const userAge = getUserAge(user);
- // add in url for amplitude events as reload has it automatically added
- const dataWithUrl = {
- url: window.location.href,
- user_age: userAge,
- organization_age: orgAge,
- ...data,
- };
- trackAmplitudeEvent(eventName, organization_id, dataWithUrl, {time});
- trackPendoEvent(eventName, data);
- }
- // using the eventName for marketing event names
- if (eventName && MARKETING_EVENT_NAMES.has(eventName)) {
- trackMarketingEvent(eventName, {plan: subscription?.plan});
- }
- } catch (err) {
- // eslint-disable-next-line no-console
- console.error('Error tracking analytics event', err);
- }
- }
|