From da23d1402f93bb63bfa6d543b165b9d68365a4bf Mon Sep 17 00:00:00 2001 From: Robbie Date: Mon, 6 Jan 2025 14:30:52 +0000 Subject: [PATCH] feat(web-analytics): Support screen events in web analytics queries (#27242) --- frontend/src/lib/constants.tsx | 1 + frontend/src/queries/schema.json | 1 + frontend/src/queries/schema.ts | 1 + .../web-analytics/tiles/WebAnalyticsTile.tsx | 4 + .../web-analytics/webAnalyticsLogic.tsx | 302 +++++++++--------- .../web_analytics/stats_table.py | 12 +- .../web_analytics/test/test_web_overview.py | 55 +++- .../test/test_web_stats_table.py | 34 +- .../web_analytics_query_runner.py | 11 +- .../hogql_queries/web_analytics/web_goals.py | 2 +- .../web_analytics/web_overview.py | 17 +- posthog/schema.py | 1 + 12 files changed, 282 insertions(+), 159 deletions(-) diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 8c934740795e4..8957e6f952763 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -237,6 +237,7 @@ export const FEATURE_FLAGS = { CDP_ACTIVITY_LOG_NOTIFICATIONS: 'cdp-activity-log-notifications', // owner: #team-cdp COOKIELESS_SERVER_HASH_MODE_SETTING: 'cookieless-server-hash-mode-setting', // owner: @robbie-c #team-web-analytics INSIGHT_COLORS: 'insight-colors', // owner @thmsobrmlr #team-product-analytics + WEB_ANALYTICS_FOR_MOBILE: 'web-analytics-for-mobile', // owner @robbie-c #team-web-analytics } as const export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS] diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index dc8c5f7d58221..fb7873e86a997 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -13402,6 +13402,7 @@ "InitialPage", "ExitPage", "ExitClick", + "ScreenName", "InitialChannelType", "InitialReferringDomain", "InitialUTMSource", diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 788793f20755f..bb7b5708b1e97 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -1850,6 +1850,7 @@ export enum WebStatsBreakdown { InitialPage = 'InitialPage', ExitPage = 'ExitPage', // not supported in the legacy version ExitClick = 'ExitClick', + ScreenName = 'ScreenName', InitialChannelType = 'InitialChannelType', InitialReferringDomain = 'InitialReferringDomain', InitialUTMSource = 'InitialUTMSource', diff --git a/frontend/src/scenes/web-analytics/tiles/WebAnalyticsTile.tsx b/frontend/src/scenes/web-analytics/tiles/WebAnalyticsTile.tsx index b5b04d6b419b5..2de57e7100d9f 100644 --- a/frontend/src/scenes/web-analytics/tiles/WebAnalyticsTile.tsx +++ b/frontend/src/scenes/web-analytics/tiles/WebAnalyticsTile.tsx @@ -127,6 +127,8 @@ const BreakdownValueTitle: QueryContextColumnTitleComponent = (props) => { return <>End Path case WebStatsBreakdown.ExitClick: return <>Exit Click + case WebStatsBreakdown.ScreenName: + return <>Screen Name case WebStatsBreakdown.InitialChannelType: return <>Initial Channel Type case WebStatsBreakdown.InitialReferringDomain: @@ -258,6 +260,8 @@ export const webStatsBreakdownToPropertyName = ( return { key: '$end_pathname', type: PropertyFilterType.Session } case WebStatsBreakdown.ExitClick: return { key: '$last_external_click_url', type: PropertyFilterType.Session } + case WebStatsBreakdown.ScreenName: + return { key: '$screen_name', type: PropertyFilterType.Event } case WebStatsBreakdown.InitialChannelType: return { key: '$channel_type', type: PropertyFilterType.Session } case WebStatsBreakdown.InitialReferringDomain: diff --git a/frontend/src/scenes/web-analytics/webAnalyticsLogic.tsx b/frontend/src/scenes/web-analytics/webAnalyticsLogic.tsx index 65b372d40a4b9..5053bf4bac296 100644 --- a/frontend/src/scenes/web-analytics/webAnalyticsLogic.tsx +++ b/frontend/src/scenes/web-analytics/webAnalyticsLogic.tsx @@ -182,6 +182,7 @@ export enum PathTab { INITIAL_PATH = 'INITIAL_PATH', END_PATH = 'END_PATH', EXIT_CLICK = 'EXIT_CLICK', + SCREEN_NAME = 'SCREEN_NAME', } export enum GeographyTab { @@ -559,7 +560,7 @@ export const webAnalyticsLogic = kea([ } const uniqueUserSeries: EventsNode = { - event: '$pageview', + event: featureFlags[FEATURE_FLAGS.WEB_ANALYTICS_FOR_MOBILE] ? '$screen' : '$pageview', kind: NodeKind.EventsNode, math: BaseMathType.UniqueUsers, name: 'Pageview', @@ -568,7 +569,7 @@ export const webAnalyticsLogic = kea([ const pageViewsSeries = { ...uniqueUserSeries, math: BaseMathType.TotalCount, - custom_name: 'Page views', + custom_name: featureFlags[FEATURE_FLAGS.WEB_ANALYTICS_FOR_MOBILE] ? 'Screen Views' : 'Page views', } const sessionsSeries = { ...uniqueUserSeries, @@ -766,149 +767,160 @@ export const webAnalyticsLogic = kea([ }, activeTabId: pathTab, setTabId: actions.setPathTab, - tabs: ( - [ - createTableTab( - TileId.PATHS, - PathTab.PATH, - 'Paths', - 'Path', - WebStatsBreakdown.Page, - { - includeScrollDepth: false, // TODO needs some perf work before it can be enabled - includeBounceRate: true, - doPathCleaning: !!isPathCleaningEnabled, - }, - { - showPathCleaningControls: true, - docs: { - url: 'https://posthog.com/docs/web-analytics/dashboard#paths', - title: 'Paths', - description: ( -
-

- In this view you can validate all of the paths that were - accessed in your application, regardless of when they were - accessed through the lifetime of a user session. -

- {conversionGoal ? ( -

- The conversion rate is the percentage of users who completed - the conversion goal in this specific path. -

- ) : ( -

- The{' '} - - bounce rate - {' '} - indicates the percentage of users who left your page - immediately after visiting without capturing any event. -

- )} -
- ), - }, - } - ), - createTableTab( - TileId.PATHS, - PathTab.INITIAL_PATH, - 'Entry paths', - 'Entry path', - WebStatsBreakdown.InitialPage, - { - includeBounceRate: true, - includeScrollDepth: false, - doPathCleaning: !!isPathCleaningEnabled, - }, - { - showPathCleaningControls: true, - docs: { - url: 'https://posthog.com/docs/web-analytics/dashboard#paths', - title: 'Entry Path', - description: ( -
-

- Entry paths are the paths a user session started, i.e. the first - path they saw when they opened your website. -

- {conversionGoal && ( -

- The conversion rate is the percentage of users who completed - the conversion goal after the first path in their session - being this path. -

- )} -
- ), - }, - } - ), - createTableTab( - TileId.PATHS, - PathTab.END_PATH, - 'End paths', - 'End path', - WebStatsBreakdown.ExitPage, - { - includeBounceRate: false, - includeScrollDepth: false, - doPathCleaning: !!isPathCleaningEnabled, - }, - { - showPathCleaningControls: true, - docs: { - url: 'https://posthog.com/docs/web-analytics/dashboard#paths', - title: 'End Path', - description: ( -
- End paths are the last path a user visited before their session - ended, i.e. the last path they saw before leaving your - website/closing the browser/turning their computer off. -
- ), - }, - } - ), - { - id: PathTab.EXIT_CLICK, - title: 'Outbound link clicks', - linkText: 'Outbound clicks', - query: { - full: true, - kind: NodeKind.DataTableNode, - source: { - kind: NodeKind.WebExternalClicksTableQuery, - properties: webAnalyticsFilters, - dateRange, - compareFilter, - sampling, - limit: 10, - filterTestAccounts, - conversionGoal: featureFlags[ - FEATURE_FLAGS.WEB_ANALYTICS_CONVERSION_GOAL_FILTERS - ] - ? conversionGoal - : undefined, - stripQueryParams: shouldStripQueryParams, - }, - embedded: false, - columns: ['url', 'visitors', 'clicks'], - }, - insightProps: createInsightProps(TileId.PATHS, PathTab.END_PATH), - canOpenModal: true, - docs: { - title: 'Outbound Clicks', - description: ( -
- You'll be able to verify when someone leaves your website by clicking an - outbound link (to a separate domain) -
- ), - }, - }, - ] as (TabsTileTab | undefined)[] + tabs: (featureFlags[FEATURE_FLAGS.WEB_ANALYTICS_FOR_MOBILE] + ? [ + createTableTab( + TileId.PATHS, + PathTab.SCREEN_NAME, + 'Screens', + 'Screen', + WebStatsBreakdown.ScreenName, + {}, + {} + ), + ] + : ([ + createTableTab( + TileId.PATHS, + PathTab.PATH, + 'Paths', + 'Path', + WebStatsBreakdown.Page, + { + includeScrollDepth: false, // TODO needs some perf work before it can be enabled + includeBounceRate: true, + doPathCleaning: !!isPathCleaningEnabled, + }, + { + showPathCleaningControls: true, + docs: { + url: 'https://posthog.com/docs/web-analytics/dashboard#paths', + title: 'Paths', + description: ( +
+

+ In this view you can validate all of the paths that were + accessed in your application, regardless of when they were + accessed through the lifetime of a user session. +

+ {conversionGoal ? ( +

+ The conversion rate is the percentage of users who + completed the conversion goal in this specific path. +

+ ) : ( +

+ The{' '} + + bounce rate + {' '} + indicates the percentage of users who left your page + immediately after visiting without capturing any event. +

+ )} +
+ ), + }, + } + ), + createTableTab( + TileId.PATHS, + PathTab.INITIAL_PATH, + 'Entry paths', + 'Entry path', + WebStatsBreakdown.InitialPage, + { + includeBounceRate: true, + includeScrollDepth: false, + doPathCleaning: !!isPathCleaningEnabled, + }, + { + showPathCleaningControls: true, + docs: { + url: 'https://posthog.com/docs/web-analytics/dashboard#paths', + title: 'Entry Path', + description: ( +
+

+ Entry paths are the paths a user session started, i.e. the + first path they saw when they opened your website. +

+ {conversionGoal && ( +

+ The conversion rate is the percentage of users who + completed the conversion goal after the first path in + their session being this path. +

+ )} +
+ ), + }, + } + ), + createTableTab( + TileId.PATHS, + PathTab.END_PATH, + 'End paths', + 'End path', + WebStatsBreakdown.ExitPage, + { + includeBounceRate: false, + includeScrollDepth: false, + doPathCleaning: !!isPathCleaningEnabled, + }, + { + showPathCleaningControls: true, + docs: { + url: 'https://posthog.com/docs/web-analytics/dashboard#paths', + title: 'End Path', + description: ( +
+ End paths are the last path a user visited before their session + ended, i.e. the last path they saw before leaving your + website/closing the browser/turning their computer off. +
+ ), + }, + } + ), + { + id: PathTab.EXIT_CLICK, + title: 'Outbound link clicks', + linkText: 'Outbound clicks', + query: { + full: true, + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.WebExternalClicksTableQuery, + properties: webAnalyticsFilters, + dateRange, + compareFilter, + sampling, + limit: 10, + filterTestAccounts, + conversionGoal: featureFlags[ + FEATURE_FLAGS.WEB_ANALYTICS_CONVERSION_GOAL_FILTERS + ] + ? conversionGoal + : undefined, + stripQueryParams: shouldStripQueryParams, + }, + embedded: false, + columns: ['url', 'visitors', 'clicks'], + }, + insightProps: createInsightProps(TileId.PATHS, PathTab.END_PATH), + canOpenModal: true, + docs: { + title: 'Outbound Clicks', + description: ( +
+ You'll be able to verify when someone leaves your website by clicking + an outbound link (to a separate domain) +
+ ), + }, + }, + ] as (TabsTileTab | undefined)[]) ).filter(isNotNil), }, { diff --git a/posthog/hogql_queries/web_analytics/stats_table.py b/posthog/hogql_queries/web_analytics/stats_table.py index 8b5ff36f76df3..c63f5775c7955 100644 --- a/posthog/hogql_queries/web_analytics/stats_table.py +++ b/posthog/hogql_queries/web_analytics/stats_table.py @@ -149,7 +149,7 @@ def to_path_scroll_bounce_query(self) -> ast.SelectQuery: min(session.$start_timestamp ) AS start_timestamp FROM events WHERE and( - events.event == '$pageview', + or(events.event == '$pageview', events.event == '$screen'), breakdown_value IS NOT NULL, {inside_periods}, {event_properties}, @@ -172,7 +172,7 @@ def to_path_scroll_bounce_query(self) -> ast.SelectQuery: min(session.$start_timestamp) as start_timestamp FROM events WHERE and( - events.event == '$pageview', + or(events.event == '$pageview', events.event == '$screen'), breakdown_value IS NOT NULL, {inside_periods}, {event_properties}, @@ -204,7 +204,7 @@ def to_path_scroll_bounce_query(self) -> ast.SelectQuery: min(session.$start_timestamp) AS start_timestamp FROM events WHERE and( - or(events.event == '$pageview', events.event == '$pageleave'), + or(events.event == '$pageview', events.event == '$pageleave', events.event == '$screen'), breakdown_value IS NOT NULL, {inside_periods}, {event_properties_for_scroll}, @@ -263,7 +263,7 @@ def to_path_bounce_query(self) -> ast.SelectQuery: min(session.$start_timestamp) AS start_timestamp FROM events WHERE and( - events.event == '$pageview', + or(events.event == '$pageview', events.event == '$screen'), {inside_periods}, {event_properties}, {session_properties}, @@ -286,7 +286,7 @@ def to_path_bounce_query(self) -> ast.SelectQuery: min(session.$start_timestamp) AS start_timestamp FROM events WHERE and( - events.event == '$pageview', + or(events.event == '$pageview', events.event == '$screen'), breakdown_value IS NOT NULL, {inside_periods}, {event_properties}, @@ -481,6 +481,8 @@ def _counts_breakdown_value(self): return self._apply_path_cleaning(ast.Field(chain=["session", "$end_pathname"])) case WebStatsBreakdown.EXIT_CLICK: return ast.Field(chain=["session", "$last_external_click_url"]) + case WebStatsBreakdown.SCREEN_NAME: + return ast.Field(chain=["events", "properties", "$screen_name"]) case WebStatsBreakdown.INITIAL_REFERRING_DOMAIN: return ast.Field(chain=["session", "$entry_referring_domain"]) case WebStatsBreakdown.INITIAL_UTM_SOURCE: diff --git a/posthog/hogql_queries/web_analytics/test/test_web_overview.py b/posthog/hogql_queries/web_analytics/test/test_web_overview.py index 3a5c421cb890a..4024e99f3c6fb 100644 --- a/posthog/hogql_queries/web_analytics/test/test_web_overview.py +++ b/posthog/hogql_queries/web_analytics/test/test_web_overview.py @@ -16,6 +16,7 @@ HogQLQueryModifiers, CustomEventConversionGoal, ActionConversionGoal, + BounceRatePageViewMode, ) from posthog.settings import HOGQL_INCREASED_MAX_EXECUTION_TIME from posthog.test.base import ( @@ -79,8 +80,11 @@ def _run_web_overview_query( action: Optional[Action] = None, custom_event: Optional[str] = None, includeLCPScore: Optional[bool] = False, + bounce_rate_mode: Optional[BounceRatePageViewMode] = BounceRatePageViewMode.COUNT_PAGEVIEWS, ): - modifiers = HogQLQueryModifiers(sessionTableVersion=session_table_version) + modifiers = HogQLQueryModifiers( + sessionTableVersion=session_table_version, bounceRatePageViewMode=bounce_rate_mode + ) query = WebOverviewQuery( dateRange=DateRange(date_from=date_from, date_to=date_to), properties=[], @@ -185,6 +189,55 @@ def test_increase_in_users(self): self.assertEqual(0, bounce.previous) self.assertEqual(None, bounce.changeFromPreviousPct) + def test_increase_in_users_using_mobile(self): + s1a = str(uuid7("2023-12-02")) + s1b = str(uuid7("2023-12-12")) + s2 = str(uuid7("2023-12-11")) + + self._create_events( + [ + ("p1", [("2023-12-02", s1a), ("2023-12-03", s1a), ("2023-12-12", s1b)]), + ("p2", [("2023-12-11", s2)]), + ], + event="$screen", + ) + + results = self._run_web_overview_query( + "2023-12-08", + "2023-12-15", + bounce_rate_mode=BounceRatePageViewMode.UNIQ_PAGE_SCREEN_AUTOCAPTURES, # bounce rate won't work in the other modes + ).results + + visitors = results[0] + self.assertEqual("visitors", visitors.key) + self.assertEqual(2, visitors.value) + self.assertEqual(1, visitors.previous) + self.assertEqual(100, visitors.changeFromPreviousPct) + + views = results[1] + self.assertEqual("views", views.key) + self.assertEqual(2, views.value) + self.assertEqual(2, views.previous) + self.assertEqual(0, views.changeFromPreviousPct) + + sessions = results[2] + self.assertEqual("sessions", sessions.key) + self.assertEqual(2, sessions.value) + self.assertEqual(1, sessions.previous) + self.assertEqual(100, sessions.changeFromPreviousPct) + + duration_s = results[3] + self.assertEqual("session duration", duration_s.key) + self.assertEqual(0, duration_s.value) + self.assertEqual(60 * 60 * 24, duration_s.previous) + self.assertEqual(-100, duration_s.changeFromPreviousPct) + + bounce = results[4] + self.assertEqual("bounce rate", bounce.key) + self.assertEqual(100, bounce.value) + self.assertEqual(0, bounce.previous) + self.assertEqual(None, bounce.changeFromPreviousPct) + def test_all_time(self): s1a = str(uuid7("2023-12-02")) s1b = str(uuid7("2023-12-12")) diff --git a/posthog/hogql_queries/web_analytics/test/test_web_stats_table.py b/posthog/hogql_queries/web_analytics/test/test_web_stats_table.py index 74c12e30586cf..84a9f650222ad 100644 --- a/posthog/hogql_queries/web_analytics/test/test_web_stats_table.py +++ b/posthog/hogql_queries/web_analytics/test/test_web_stats_table.py @@ -16,6 +16,7 @@ HogQLQueryModifiers, CustomEventConversionGoal, ActionConversionGoal, + BounceRatePageViewMode, ) from posthog.test.base import ( APIBaseTest, @@ -44,8 +45,11 @@ def _create_events(self, data, event="$pageview"): for timestamp, session_id, *extra in timestamps: url = None elements = None + screen_name = None if event == "$pageview": url = extra[0] if extra else None + elif event == "$screen": + screen_name = extra[0] if extra else None elif event == "$autocapture": elements = extra[0] if extra else None properties = extra[1] if extra and len(extra) > 1 else {} @@ -59,6 +63,7 @@ def _create_events(self, data, event="$pageview"): "$session_id": session_id, "$pathname": url, "$current_url": url, + "$screen_name": screen_name, **properties, }, elements=elements, @@ -129,8 +134,11 @@ def _run_web_stats_table_query( custom_event: Optional[str] = None, session_table_version: SessionTableVersion = SessionTableVersion.V2, filter_test_accounts: Optional[bool] = False, + bounce_rate_mode: Optional[BounceRatePageViewMode] = BounceRatePageViewMode.COUNT_PAGEVIEWS, ): - modifiers = HogQLQueryModifiers(sessionTableVersion=session_table_version) + modifiers = HogQLQueryModifiers( + sessionTableVersion=session_table_version, bounceRatePageViewMode=bounce_rate_mode + ) query = WebStatsTableQuery( dateRange=DateRange(date_from=date_from, date_to=date_to), properties=properties or [], @@ -179,6 +187,30 @@ def test_increase_in_users(self): results, ) + def test_increase_in_users_on_mobile(self): + s1a = str(uuid7("2023-12-02")) + s1b = str(uuid7("2023-12-13")) + s2 = str(uuid7("2023-12-10")) + self._create_events( + [ + ("p1", [("2023-12-02", s1a, "Home"), ("2023-12-03", s1a, "Login"), ("2023-12-13", s1b, "Docs")]), + ("p2", [("2023-12-10", s2, "Home")]), + ], + event="$screen", + ) + + results = self._run_web_stats_table_query( + "2023-12-01", "2023-12-11", breakdown_by=WebStatsBreakdown.SCREEN_NAME + ).results + + self.assertEqual( + [ + ["Home", (2, None), (2, None)], + ["Login", (1, None), (1, None)], + ], + results, + ) + def test_all_time(self): s1a = str(uuid7("2023-12-02")) s1b = str(uuid7("2023-12-13")) diff --git a/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py b/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py index 5d848a5057c57..28d87ecb0be1e 100644 --- a/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py +++ b/posthog/hogql_queries/web_analytics/web_analytics_query_runner.py @@ -172,8 +172,15 @@ def conversion_person_id_expr(self) -> Optional[ast.Expr]: @cached_property def event_type_expr(self) -> ast.Expr: - pageview_expr = ast.CompareOperation( - op=ast.CompareOperationOp.Eq, left=ast.Field(chain=["event"]), right=ast.Constant(value="$pageview") + pageview_expr = ast.Or( + exprs=[ + ast.CompareOperation( + op=ast.CompareOperationOp.Eq, left=ast.Field(chain=["event"]), right=ast.Constant(value="$pageview") + ), + ast.CompareOperation( + op=ast.CompareOperationOp.Eq, left=ast.Field(chain=["event"]), right=ast.Constant(value="$screen") + ), + ] ) if self.conversion_goal_expr: diff --git a/posthog/hogql_queries/web_analytics/web_goals.py b/posthog/hogql_queries/web_analytics/web_goals.py index a89c933a369c8..1fa1511edb95f 100644 --- a/posthog/hogql_queries/web_analytics/web_goals.py +++ b/posthog/hogql_queries/web_analytics/web_goals.py @@ -143,7 +143,7 @@ def to_query(self) -> ast.SelectQuery | ast.SelectSetQuery: FROM events WHERE and( events.`$session_id` IS NOT NULL, - event = '$pageview' OR {action_where}, + event = '$pageview' OR event = '$screen' OR {action_where}, {periods_expression}, {event_properties}, {session_properties} diff --git a/posthog/hogql_queries/web_analytics/web_overview.py b/posthog/hogql_queries/web_analytics/web_overview.py index 8eb58f32adefb..032fc7615de08 100644 --- a/posthog/hogql_queries/web_analytics/web_overview.py +++ b/posthog/hogql_queries/web_analytics/web_overview.py @@ -88,10 +88,19 @@ def pageview_count_expression(self) -> ast.Expr: return ast.Call( name="countIf", args=[ - ast.CompareOperation( - left=ast.Field(chain=["event"]), - op=ast.CompareOperationOp.Eq, - right=ast.Constant(value="$pageview"), + ast.Or( + exprs=[ + ast.CompareOperation( + left=ast.Field(chain=["event"]), + op=ast.CompareOperationOp.Eq, + right=ast.Constant(value="$pageview"), + ), + ast.CompareOperation( + left=ast.Field(chain=["event"]), + op=ast.CompareOperationOp.Eq, + right=ast.Constant(value="$screen"), + ), + ] ) ], ) diff --git a/posthog/schema.py b/posthog/schema.py index 1dddae4e22282..ffb3a0154190c 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -1717,6 +1717,7 @@ class WebStatsBreakdown(StrEnum): INITIAL_PAGE = "InitialPage" EXIT_PAGE = "ExitPage" EXIT_CLICK = "ExitClick" + SCREEN_NAME = "ScreenName" INITIAL_CHANNEL_TYPE = "InitialChannelType" INITIAL_REFERRING_DOMAIN = "InitialReferringDomain" INITIAL_UTM_SOURCE = "InitialUTMSource"