Add Stripe-based logical subscriptions ETLs (DENG-977) (#4166)
* Add `subscription_platform_derived.services_v1` ETL. * Add `subscription_platform_derived.subplat_flow_events_v1` ETL. * Add `subscription_platform_derived.subplat_attribution_impressions_v1` ETL. * Add `mozilla_vpn_derived.users_attribution_v1` table. * Add `subscription_platform_derived.stripe_logical_subscriptions_history_v1` ETL. * Add `subscription_platform_derived.logical_subscriptions_history_v1` ETL. * Add `subscription_platform_derived.daily_active_logical_subscriptions_v1` ETL. * Add `subscription_platform_derived.monthly_active_logical_subscriptions_v1` ETL. * Add `subscription_platform_derived.logical_subscription_events_v1` ETL. * Add `subscription_platform.logical_subscriptions` view. * Add `subscription_platform.daily_active_logical_subscriptions` view. * Add `subscription_platform.monthly_active_logical_subscriptions` view. * Add `subscription_platform.logical_subscription_events` view.
This commit is contained in:
Родитель
8e751a26da
Коммит
8f4a06a462
|
@ -98,7 +98,9 @@ dry_run:
|
|||
- sql/moz-fx-data-shared-prod/subscription_platform_derived/nonprod_apple_subscriptions_v1/query.sql
|
||||
- sql/moz-fx-data-shared-prod/subscription_platform_derived/nonprod_google_subscriptions_v1/query.sql
|
||||
- sql/moz-fx-data-shared-prod/subscription_platform_derived/nonprod_stripe_subscriptions_history_v1/query.sql
|
||||
- sql/moz-fx-data-shared-prod/subscription_platform_derived/services_v1/query.sql
|
||||
- sql/moz-fx-data-shared-prod/subscription_platform_derived/stripe_customers_revised_changelog_v1/query.sql
|
||||
- sql/moz-fx-data-shared-prod/subscription_platform_derived/stripe_logical_subscriptions_history_v1/query.sql
|
||||
- sql/moz-fx-data-shared-prod/subscription_platform_derived/stripe_subscriptions_revised_changelog_v1/query.sql
|
||||
- sql/moz-fx-data-shared-prod/subscription_platform_derived/stripe_subscriptions_history_v1/query.sql
|
||||
- sql/moz-fx-data-shared-prod/stripe/itemized_payout_reconciliation/view.sql
|
||||
|
@ -113,6 +115,7 @@ dry_run:
|
|||
- sql/moz-fx-data-shared-prod/mozilla_vpn_derived/site_metrics_empty_check_v1/query.sql
|
||||
- sql/moz-fx-data-shared-prod/mozilla_vpn_derived/site_metrics_summary_v1/query.sql
|
||||
- sql/moz-fx-data-shared-prod/mozilla_vpn_derived/subscriptions_v1/query.sql
|
||||
- sql/moz-fx-data-shared-prod/mozilla_vpn_derived/users_attribution_v1/query.sql
|
||||
- sql/moz-fx-data-shared-prod/mozilla_vpn_derived/users_v1/query.sql
|
||||
- sql/moz-fx-data-shared-prod/mozilla_vpn_external/devices_v1/query.sql
|
||||
- sql/moz-fx-data-shared-prod/mozilla_vpn_external/subscriptions_v1/query.sql
|
||||
|
|
|
@ -963,6 +963,17 @@ with DAG(
|
|||
task_concurrency=1,
|
||||
)
|
||||
|
||||
subscription_platform_derived__daily_active_logical_subscriptions__v1 = bigquery_etl_query(
|
||||
task_id="subscription_platform_derived__daily_active_logical_subscriptions__v1",
|
||||
destination_table="daily_active_logical_subscriptions_v1",
|
||||
dataset_id="subscription_platform_derived",
|
||||
project_id="moz-fx-data-shared-prod",
|
||||
owner="srose@mozilla.com",
|
||||
email=["srose@mozilla.com", "telemetry-alerts@mozilla.com"],
|
||||
date_partition_parameter="date",
|
||||
depends_on_past=False,
|
||||
)
|
||||
|
||||
subscription_platform_derived__google_subscriptions__v1 = bigquery_etl_query(
|
||||
task_id="subscription_platform_derived__google_subscriptions__v1",
|
||||
destination_table="google_subscriptions_v1",
|
||||
|
@ -975,6 +986,43 @@ with DAG(
|
|||
task_concurrency=1,
|
||||
)
|
||||
|
||||
subscription_platform_derived__logical_subscription_events__v1 = bigquery_etl_query(
|
||||
task_id="subscription_platform_derived__logical_subscription_events__v1",
|
||||
destination_table="logical_subscription_events_v1",
|
||||
dataset_id="subscription_platform_derived",
|
||||
project_id="moz-fx-data-shared-prod",
|
||||
owner="srose@mozilla.com",
|
||||
email=["srose@mozilla.com", "telemetry-alerts@mozilla.com"],
|
||||
date_partition_parameter="date",
|
||||
depends_on_past=False,
|
||||
)
|
||||
|
||||
subscription_platform_derived__logical_subscriptions_history__v1 = (
|
||||
bigquery_etl_query(
|
||||
task_id="subscription_platform_derived__logical_subscriptions_history__v1",
|
||||
destination_table="logical_subscriptions_history_v1",
|
||||
dataset_id="subscription_platform_derived",
|
||||
project_id="moz-fx-data-shared-prod",
|
||||
owner="srose@mozilla.com",
|
||||
email=["srose@mozilla.com", "telemetry-alerts@mozilla.com"],
|
||||
date_partition_parameter=None,
|
||||
depends_on_past=False,
|
||||
task_concurrency=1,
|
||||
)
|
||||
)
|
||||
|
||||
subscription_platform_derived__monthly_active_logical_subscriptions__v1 = bigquery_etl_query(
|
||||
task_id="subscription_platform_derived__monthly_active_logical_subscriptions__v1",
|
||||
destination_table="monthly_active_logical_subscriptions_v1",
|
||||
dataset_id="subscription_platform_derived",
|
||||
project_id="moz-fx-data-shared-prod",
|
||||
owner="srose@mozilla.com",
|
||||
email=["srose@mozilla.com", "telemetry-alerts@mozilla.com"],
|
||||
date_partition_parameter="date",
|
||||
table_partition_template='${{ dag_run.logical_date.strftime("%Y%m") }}',
|
||||
depends_on_past=False,
|
||||
)
|
||||
|
||||
subscription_platform_derived__nonprod_apple_subscriptions__v1 = bigquery_etl_query(
|
||||
task_id="subscription_platform_derived__nonprod_apple_subscriptions__v1",
|
||||
destination_table="nonprod_apple_subscriptions_v1",
|
||||
|
@ -1001,6 +1049,18 @@ with DAG(
|
|||
)
|
||||
)
|
||||
|
||||
subscription_platform_derived__services__v1 = bigquery_etl_query(
|
||||
task_id="subscription_platform_derived__services__v1",
|
||||
destination_table="services_v1",
|
||||
dataset_id="subscription_platform_derived",
|
||||
project_id="moz-fx-data-shared-prod",
|
||||
owner="srose@mozilla.com",
|
||||
email=["srose@mozilla.com", "telemetry-alerts@mozilla.com"],
|
||||
date_partition_parameter=None,
|
||||
depends_on_past=False,
|
||||
task_concurrency=1,
|
||||
)
|
||||
|
||||
subscription_platform_derived__stripe_customers_history__v1 = bigquery_etl_query(
|
||||
task_id="subscription_platform_derived__stripe_customers_history__v1",
|
||||
destination_table="stripe_customers_history_v1",
|
||||
|
@ -1024,6 +1084,18 @@ with DAG(
|
|||
depends_on_past=False,
|
||||
)
|
||||
|
||||
subscription_platform_derived__stripe_logical_subscriptions_history__v1 = bigquery_etl_query(
|
||||
task_id="subscription_platform_derived__stripe_logical_subscriptions_history__v1",
|
||||
destination_table="stripe_logical_subscriptions_history_v1",
|
||||
dataset_id="subscription_platform_derived",
|
||||
project_id="moz-fx-data-shared-prod",
|
||||
owner="srose@mozilla.com",
|
||||
email=["srose@mozilla.com", "telemetry-alerts@mozilla.com"],
|
||||
date_partition_parameter=None,
|
||||
depends_on_past=False,
|
||||
task_concurrency=1,
|
||||
)
|
||||
|
||||
subscription_platform_derived__stripe_subscriptions__v1 = bigquery_etl_query(
|
||||
task_id="subscription_platform_derived__stripe_subscriptions__v1",
|
||||
destination_table="stripe_subscriptions_v1",
|
||||
|
@ -1075,6 +1147,29 @@ with DAG(
|
|||
depends_on_past=False,
|
||||
)
|
||||
|
||||
subscription_platform_derived__subplat_attribution_impressions__v1 = bigquery_etl_query(
|
||||
task_id="subscription_platform_derived__subplat_attribution_impressions__v1",
|
||||
destination_table="subplat_attribution_impressions_v1",
|
||||
dataset_id="subscription_platform_derived",
|
||||
project_id="moz-fx-data-shared-prod",
|
||||
owner="srose@mozilla.com",
|
||||
email=["srose@mozilla.com", "telemetry-alerts@mozilla.com"],
|
||||
date_partition_parameter=None,
|
||||
depends_on_past=True,
|
||||
parameters=["date:DATE:{{ds}}"],
|
||||
)
|
||||
|
||||
subscription_platform_derived__subplat_flow_events__v1 = bigquery_etl_query(
|
||||
task_id="subscription_platform_derived__subplat_flow_events__v1",
|
||||
destination_table="subplat_flow_events_v1",
|
||||
dataset_id="subscription_platform_derived",
|
||||
project_id="moz-fx-data-shared-prod",
|
||||
owner="srose@mozilla.com",
|
||||
email=["srose@mozilla.com", "telemetry-alerts@mozilla.com"],
|
||||
date_partition_parameter="date",
|
||||
depends_on_past=True,
|
||||
)
|
||||
|
||||
wait_for_firefox_accounts_derived__fxa_auth_events__v1 = ExternalTaskSensor(
|
||||
task_id="wait_for_firefox_accounts_derived__fxa_auth_events__v1",
|
||||
external_dag_id="bqetl_fxa_events",
|
||||
|
@ -1435,10 +1530,40 @@ with DAG(
|
|||
mozilla_vpn_derived__guardian_apple_events__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__daily_active_logical_subscriptions__v1.set_upstream(
|
||||
subscription_platform_derived__logical_subscriptions_history__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__logical_subscription_events__v1.set_upstream(
|
||||
subscription_platform_derived__logical_subscriptions_history__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__logical_subscriptions_history__v1.set_upstream(
|
||||
mozilla_vpn_derived__users__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__logical_subscriptions_history__v1.set_upstream(
|
||||
subscription_platform_derived__stripe_logical_subscriptions_history__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__logical_subscriptions_history__v1.set_upstream(
|
||||
subscription_platform_derived__subplat_attribution_impressions__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__monthly_active_logical_subscriptions__v1.set_upstream(
|
||||
subscription_platform_derived__daily_active_logical_subscriptions__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__nonprod_apple_subscriptions__v1.set_upstream(
|
||||
mozilla_vpn_derived__guardian_apple_events__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__services__v1.set_upstream(stripe_external__plan__v1)
|
||||
|
||||
subscription_platform_derived__services__v1.set_upstream(
|
||||
stripe_external__product__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__stripe_customers_history__v1.set_upstream(
|
||||
subscription_platform_derived__stripe_customers_revised_changelog__v1
|
||||
)
|
||||
|
@ -1451,6 +1576,30 @@ with DAG(
|
|||
stripe_external__subscriptions_changelog__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__stripe_logical_subscriptions_history__v1.set_upstream(
|
||||
stripe_external__card__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__stripe_logical_subscriptions_history__v1.set_upstream(
|
||||
stripe_external__charge__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__stripe_logical_subscriptions_history__v1.set_upstream(
|
||||
stripe_external__invoice__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__stripe_logical_subscriptions_history__v1.set_upstream(
|
||||
stripe_external__refund__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__stripe_logical_subscriptions_history__v1.set_upstream(
|
||||
subscription_platform_derived__services__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__stripe_logical_subscriptions_history__v1.set_upstream(
|
||||
subscription_platform_derived__stripe_subscriptions_history__v2
|
||||
)
|
||||
|
||||
subscription_platform_derived__stripe_subscriptions__v1.set_upstream(
|
||||
subscription_platform_derived__stripe_subscriptions_history__v1
|
||||
)
|
||||
|
@ -1522,3 +1671,25 @@ with DAG(
|
|||
subscription_platform_derived__stripe_subscriptions_revised_changelog__v1.set_upstream(
|
||||
stripe_external__subscriptions_changelog__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__subplat_attribution_impressions__v1.set_upstream(
|
||||
subscription_platform_derived__services__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__subplat_attribution_impressions__v1.set_upstream(
|
||||
subscription_platform_derived__subplat_flow_events__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__subplat_flow_events__v1.set_upstream(
|
||||
wait_for_firefox_accounts_derived__fxa_auth_events__v1
|
||||
)
|
||||
subscription_platform_derived__subplat_flow_events__v1.set_upstream(
|
||||
wait_for_firefox_accounts_derived__fxa_content_events__v1
|
||||
)
|
||||
subscription_platform_derived__subplat_flow_events__v1.set_upstream(
|
||||
wait_for_firefox_accounts_derived__fxa_stdout_events__v1
|
||||
)
|
||||
|
||||
subscription_platform_derived__subplat_flow_events__v1.set_upstream(
|
||||
subscription_platform_derived__services__v1
|
||||
)
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
friendly_name: Mozilla VPN users attribution archive
|
||||
description: |-
|
||||
Archive of VPN users' attribution data between 2020-06-25 and 2022-03-23.
|
||||
|
||||
The attribution data was originally stored in the Guardian database's `users` table and synced into `mozilla_vpn_external.users_v1`.
|
||||
It was later copied into the undocumented `mozilla_vpn_external.users_attribution_v1` table with the attribution data as JSON strings.
|
||||
Now it's officially archived here in a fully structured table.
|
||||
owners:
|
||||
- srose@mozilla.com
|
||||
labels:
|
||||
incremental: false
|
||||
bigquery:
|
||||
time_partitioning: null
|
||||
clustering: null
|
||||
references: {}
|
|
@ -0,0 +1,31 @@
|
|||
WITH users_attribution AS (
|
||||
SELECT
|
||||
id AS user_id,
|
||||
STRUCT(
|
||||
NULLIF(JSON_VALUE(attribution, '$.referrer'), '') AS referrer,
|
||||
JSON_VALUE(attribution, '$.entrypoint_experiment') AS entrypoint_experiment,
|
||||
JSON_VALUE(attribution, '$.entrypoint_variation') AS entrypoint_variation,
|
||||
JSON_VALUE(attribution, '$.utm_campaign') AS utm_campaign,
|
||||
JSON_VALUE(attribution, '$.utm_content') AS utm_content,
|
||||
NULLIF(NULLIF(JSON_VALUE(attribution, '$.utm_medium'), ''), '(not set)') AS utm_medium,
|
||||
NULLIF(NULLIF(JSON_VALUE(attribution, '$.utm_source'), ''), '(not set)') AS utm_source,
|
||||
JSON_VALUE(attribution, '$.utm_term') AS utm_term,
|
||||
JSON_VALUE(attribution, '$.data_cta_position') AS data_cta_position
|
||||
) AS attribution
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.mozilla_vpn_external.users_attribution_v1`
|
||||
)
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
users_attribution
|
||||
WHERE
|
||||
attribution.referrer IS NOT NULL
|
||||
OR attribution.entrypoint_experiment IS NOT NULL
|
||||
OR attribution.entrypoint_variation IS NOT NULL
|
||||
OR attribution.utm_campaign IS NOT NULL
|
||||
OR attribution.utm_content IS NOT NULL
|
||||
OR attribution.utm_medium IS NOT NULL
|
||||
OR attribution.utm_source IS NOT NULL
|
||||
OR attribution.utm_term IS NOT NULL
|
||||
OR attribution.data_cta_position IS NOT NULL
|
|
@ -0,0 +1,35 @@
|
|||
fields:
|
||||
- name: user_id
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: attribution
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: referrer
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_experiment
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_variation
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_campaign
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_content
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_medium
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_source
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_term
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: data_cta_position
|
||||
type: STRING
|
||||
mode: NULLABLE
|
|
@ -0,0 +1,7 @@
|
|||
CREATE OR REPLACE VIEW
|
||||
`moz-fx-data-shared-prod.subscription_platform.daily_active_logical_subscriptions`
|
||||
AS
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.daily_active_logical_subscriptions_v1`
|
|
@ -0,0 +1,7 @@
|
|||
CREATE OR REPLACE VIEW
|
||||
`moz-fx-data-shared-prod.subscription_platform.logical_subscription_events`
|
||||
AS
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.logical_subscription_events_v1`
|
|
@ -0,0 +1,9 @@
|
|||
CREATE OR REPLACE VIEW
|
||||
`moz-fx-data-shared-prod.subscription_platform.logical_subscriptions`
|
||||
AS
|
||||
SELECT
|
||||
subscription.*
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.logical_subscriptions_history_v1`
|
||||
WHERE
|
||||
valid_to = '9999-12-31 23:59:59.999999'
|
|
@ -0,0 +1,7 @@
|
|||
CREATE OR REPLACE VIEW
|
||||
`moz-fx-data-shared-prod.subscription_platform.monthly_active_logical_subscriptions`
|
||||
AS
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.monthly_active_logical_subscriptions_v1`
|
|
@ -0,0 +1,22 @@
|
|||
friendly_name: Daily active logical subscriptions
|
||||
description: |-
|
||||
Daily snapshots of logical subscriptions that were active at any point during each day.
|
||||
The latest state of the subscription during the day is saved.
|
||||
|
||||
Logical subscriptions are a continuous active period for a particular provider subscription.
|
||||
owners:
|
||||
- srose@mozilla.com
|
||||
labels:
|
||||
incremental: true
|
||||
schedule: daily
|
||||
scheduling:
|
||||
dag_name: bqetl_subplat
|
||||
date_partition_parameter: date
|
||||
bigquery:
|
||||
time_partitioning:
|
||||
type: day
|
||||
field: date
|
||||
require_partition_filter: false
|
||||
expiration_days: null
|
||||
clustering: null
|
||||
references: {}
|
|
@ -0,0 +1,61 @@
|
|||
WITH dates AS (
|
||||
{% if is_init() %}
|
||||
SELECT
|
||||
`date`,
|
||||
(`date` + 1) AS next_date
|
||||
FROM
|
||||
UNNEST(
|
||||
GENERATE_DATE_ARRAY(
|
||||
(
|
||||
SELECT
|
||||
DATE(MIN(started_at))
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform.logical_subscriptions`
|
||||
),
|
||||
CURRENT_DATE() - 1
|
||||
)
|
||||
) AS `date`
|
||||
{% else %}
|
||||
SELECT
|
||||
@date AS `date`,
|
||||
(@date + 1) AS next_date
|
||||
{% endif %}
|
||||
),
|
||||
daily_active_subscriptions_history AS (
|
||||
SELECT
|
||||
CONCAT(subscriptions_history.subscription.id, '-', FORMAT_DATE('%F', dates.date)) AS id,
|
||||
dates.date,
|
||||
MIN_BY(
|
||||
subscriptions_history,
|
||||
subscriptions_history.valid_from
|
||||
) AS earliest_subscription_history,
|
||||
MAX_BY(subscriptions_history, subscriptions_history.valid_from) AS latest_subscription_history
|
||||
FROM
|
||||
dates
|
||||
JOIN
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.logical_subscriptions_history_v1` AS subscriptions_history
|
||||
ON
|
||||
TIMESTAMP(dates.next_date) > subscriptions_history.valid_from
|
||||
AND TIMESTAMP(dates.date) < subscriptions_history.valid_to
|
||||
AND (
|
||||
TIMESTAMP(dates.date) < subscriptions_history.subscription.ended_at
|
||||
OR subscriptions_history.subscription.ended_at IS NULL
|
||||
)
|
||||
GROUP BY
|
||||
dates.date,
|
||||
subscriptions_history.subscription.id
|
||||
HAVING
|
||||
LOGICAL_OR(subscriptions_history.subscription.is_active)
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
`date`,
|
||||
latest_subscription_history.id AS logical_subscriptions_history_id,
|
||||
latest_subscription_history.subscription,
|
||||
(
|
||||
earliest_subscription_history.subscription.is_active
|
||||
AND earliest_subscription_history.valid_from <= TIMESTAMP(`date`)
|
||||
) AS was_active_at_day_start,
|
||||
latest_subscription_history.subscription.is_active AS was_active_at_day_end
|
||||
FROM
|
||||
daily_active_subscriptions_history
|
|
@ -0,0 +1,200 @@
|
|||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: date
|
||||
type: DATE
|
||||
mode: NULLABLE
|
||||
- name: logical_subscriptions_history_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: subscription
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: payment_provider
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_item_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_created_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_updated_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: provider_customer_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: mozilla_account_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: mozilla_account_id_sha256
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: customer_subscription_number
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: country_code
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: country_name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: services
|
||||
type: RECORD
|
||||
mode: REPEATED
|
||||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: tier
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_product_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: product_name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_plan_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_summary
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_type
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_count
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_months
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: plan_currency
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_amount
|
||||
type: NUMERIC
|
||||
mode: NULLABLE
|
||||
- name: is_bundle
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: is_trial
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: is_active
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: provider_status
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: started_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: ended_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: current_period_started_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: current_period_ends_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: auto_renew
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: auto_renew_disabled_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: has_refunds
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: has_fraudulent_charges
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: first_touch_attribution
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: impression_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: entrypoint
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_experiment
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_variation
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_campaign
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_content
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_medium
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_source
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_term
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: last_touch_attribution
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: impression_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: entrypoint
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_experiment
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_variation
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_campaign
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_content
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_medium
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_source
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_term
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: was_active_at_day_start
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: was_active_at_day_end
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
|
@ -0,0 +1,24 @@
|
|||
friendly_name: Logical subscriptions events
|
||||
description: |-
|
||||
Logical subscription events such as "Subscription Start", "Plan Change", "Auto-Renew Change", and "Subscription End".
|
||||
|
||||
Logical subscriptions are a continuous active period for a particular provider subscription.
|
||||
owners:
|
||||
- srose@mozilla.com
|
||||
labels:
|
||||
incremental: true
|
||||
schedule: daily
|
||||
scheduling:
|
||||
dag_name: bqetl_subplat
|
||||
date_partition_parameter: date
|
||||
bigquery:
|
||||
time_partitioning:
|
||||
type: day
|
||||
field: timestamp
|
||||
require_partition_filter: false
|
||||
expiration_days: null
|
||||
clustering:
|
||||
fields:
|
||||
- type
|
||||
- reason
|
||||
references: {}
|
|
@ -0,0 +1,175 @@
|
|||
WITH subscription_changes AS (
|
||||
SELECT
|
||||
id AS logical_subscriptions_history_id,
|
||||
valid_from AS `timestamp`,
|
||||
subscription,
|
||||
LAG(subscription) OVER (PARTITION BY subscription.id ORDER BY valid_from) AS old_subscription
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.logical_subscriptions_history_v1`
|
||||
WHERE
|
||||
{% if is_init() %}
|
||||
DATE(valid_from) < CURRENT_DATE()
|
||||
{% else %}
|
||||
DATE(valid_from) = @date
|
||||
OR DATE(valid_to) = @date
|
||||
{% endif %}
|
||||
),
|
||||
subscription_start_events AS (
|
||||
SELECT
|
||||
subscription.started_at AS `timestamp`,
|
||||
'Subscription Start' AS type,
|
||||
CONCAT(
|
||||
IF(subscription.customer_subscription_number = 1, 'New Customer', 'Returning Customer'),
|
||||
IF(subscription.is_trial, ' Trial', '')
|
||||
) AS reason,
|
||||
logical_subscriptions_history_id,
|
||||
subscription,
|
||||
old_subscription
|
||||
FROM
|
||||
subscription_changes
|
||||
QUALIFY
|
||||
1 = ROW_NUMBER() OVER (PARTITION BY subscription.id ORDER BY `timestamp`)
|
||||
{% if not is_init() %}
|
||||
AND DATE(subscription.started_at) = @date
|
||||
{% endif %}
|
||||
),
|
||||
subscription_end_events AS (
|
||||
SELECT
|
||||
subscription.ended_at AS `timestamp`,
|
||||
'Subscription End' AS type,
|
||||
-- TODO: rather than "Unknown", determine if the user cancelled intentionally or their payment failed
|
||||
IF(NOT subscription.auto_renew, 'Auto-Renew Disabled', 'Unknown') AS reason,
|
||||
logical_subscriptions_history_id,
|
||||
subscription,
|
||||
old_subscription
|
||||
FROM
|
||||
subscription_changes
|
||||
WHERE
|
||||
subscription.ended_at IS NOT NULL
|
||||
QUALIFY
|
||||
1 = ROW_NUMBER() OVER (PARTITION BY subscription.id ORDER BY `timestamp`)
|
||||
{% if not is_init() %}
|
||||
AND DATE(subscription.ended_at) = @date
|
||||
{% endif %}
|
||||
),
|
||||
mozilla_account_change_events AS (
|
||||
SELECT
|
||||
`timestamp`,
|
||||
'Mozilla Account Change' AS type,
|
||||
CASE
|
||||
WHEN old_subscription.mozilla_account_id_sha256 IS NULL
|
||||
THEN 'Mozilla Account Added'
|
||||
WHEN subscription.mozilla_account_id_sha256 IS NULL
|
||||
THEN 'Mozilla Account Removed'
|
||||
ELSE 'Mozilla Account Changed'
|
||||
END AS reason,
|
||||
logical_subscriptions_history_id,
|
||||
subscription,
|
||||
old_subscription
|
||||
FROM
|
||||
subscription_changes
|
||||
WHERE
|
||||
old_subscription IS NOT NULL
|
||||
AND subscription.mozilla_account_id_sha256 IS DISTINCT FROM old_subscription.mozilla_account_id_sha256
|
||||
),
|
||||
plan_change_events AS (
|
||||
SELECT
|
||||
`timestamp`,
|
||||
'Plan Change' AS type,
|
||||
CASE
|
||||
WHEN (SELECT STRING_AGG(id ORDER BY id) FROM UNNEST(subscription.services)) != (
|
||||
SELECT
|
||||
STRING_AGG(id ORDER BY id)
|
||||
FROM
|
||||
UNNEST(old_subscription.services)
|
||||
)
|
||||
THEN 'Services Changed'
|
||||
WHEN (SELECT STRING_AGG(tier ORDER BY id) FROM UNNEST(subscription.services)) != (
|
||||
SELECT
|
||||
STRING_AGG(tier ORDER BY id)
|
||||
FROM
|
||||
UNNEST(old_subscription.services)
|
||||
)
|
||||
THEN 'Service Tier Changed'
|
||||
WHEN subscription.plan_interval != old_subscription.plan_interval
|
||||
OR subscription.plan_interval_count != old_subscription.plan_interval_count
|
||||
THEN 'Plan Interval Changed'
|
||||
END AS reason,
|
||||
logical_subscriptions_history_id,
|
||||
subscription,
|
||||
old_subscription
|
||||
FROM
|
||||
subscription_changes
|
||||
WHERE
|
||||
subscription.provider_plan_id != old_subscription.provider_plan_id
|
||||
),
|
||||
trial_change_events AS (
|
||||
SELECT
|
||||
`timestamp`,
|
||||
'Trial Change' AS type,
|
||||
IF(subscription.is_trial, 'Trial Started', 'Trial Converted') AS reason,
|
||||
logical_subscriptions_history_id,
|
||||
subscription,
|
||||
old_subscription
|
||||
FROM
|
||||
subscription_changes
|
||||
WHERE
|
||||
subscription.is_trial != old_subscription.is_trial
|
||||
AND subscription.ended_at IS NULL
|
||||
),
|
||||
auto_renew_change_events AS (
|
||||
SELECT
|
||||
`timestamp`,
|
||||
'Auto-Renew Change' AS type,
|
||||
IF(subscription.auto_renew, 'Auto-Renew Enabled', 'Auto-Renew Disabled') AS reason,
|
||||
logical_subscriptions_history_id,
|
||||
subscription,
|
||||
old_subscription
|
||||
FROM
|
||||
subscription_changes
|
||||
WHERE
|
||||
subscription.auto_renew != old_subscription.auto_renew
|
||||
AND subscription.ended_at IS NULL
|
||||
),
|
||||
all_events AS (
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
subscription_start_events
|
||||
UNION ALL
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
subscription_end_events
|
||||
UNION ALL
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
mozilla_account_change_events
|
||||
UNION ALL
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
plan_change_events
|
||||
UNION ALL
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
trial_change_events
|
||||
UNION ALL
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
auto_renew_change_events
|
||||
)
|
||||
SELECT
|
||||
CONCAT(
|
||||
subscription.id,
|
||||
'-',
|
||||
FORMAT_TIMESTAMP('%FT%H:%M:%E6S', `timestamp`),
|
||||
'-',
|
||||
REPLACE(type, ' ', '-')
|
||||
) AS id,
|
||||
*
|
||||
FROM
|
||||
all_events
|
|
@ -0,0 +1,384 @@
|
|||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: timestamp
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: type
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: reason
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: logical_subscriptions_history_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: subscription
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: payment_provider
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_item_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_created_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_updated_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: provider_customer_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: mozilla_account_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: mozilla_account_id_sha256
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: customer_subscription_number
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: country_code
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: country_name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: services
|
||||
type: RECORD
|
||||
mode: REPEATED
|
||||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: tier
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_product_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: product_name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_plan_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_summary
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_type
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_count
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_months
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: plan_currency
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_amount
|
||||
type: NUMERIC
|
||||
mode: NULLABLE
|
||||
- name: is_bundle
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: is_trial
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: is_active
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: provider_status
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: started_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: ended_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: current_period_started_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: current_period_ends_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: auto_renew
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: auto_renew_disabled_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: has_refunds
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: has_fraudulent_charges
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: first_touch_attribution
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: impression_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: entrypoint
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_experiment
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_variation
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_campaign
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_content
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_medium
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_source
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_term
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: last_touch_attribution
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: impression_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: entrypoint
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_experiment
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_variation
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_campaign
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_content
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_medium
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_source
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_term
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: old_subscription
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: payment_provider
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_item_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_created_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_updated_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: provider_customer_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: mozilla_account_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: mozilla_account_id_sha256
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: customer_subscription_number
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: country_code
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: country_name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: services
|
||||
type: RECORD
|
||||
mode: REPEATED
|
||||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: tier
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_product_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: product_name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_plan_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_summary
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_type
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_count
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_months
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: plan_currency
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_amount
|
||||
type: NUMERIC
|
||||
mode: NULLABLE
|
||||
- name: is_bundle
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: is_trial
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: is_active
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: provider_status
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: started_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: ended_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: current_period_started_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: current_period_ends_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: auto_renew
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: auto_renew_disabled_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: has_refunds
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: has_fraudulent_charges
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: first_touch_attribution
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: impression_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: entrypoint
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_experiment
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_variation
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_campaign
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_content
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_medium
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_source
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_term
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: last_touch_attribution
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: impression_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: entrypoint
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_experiment
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_variation
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_campaign
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_content
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_medium
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_source
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_term
|
||||
type: STRING
|
||||
mode: NULLABLE
|
|
@ -0,0 +1,26 @@
|
|||
friendly_name: Logical subscriptions history
|
||||
description: |-
|
||||
History of changes to logical subscriptions, which are a continuous active period for a particular provider subscription.
|
||||
|
||||
To get the historical state at a particular point in time use a condition like the following:
|
||||
valid_from <= {timestamp}
|
||||
AND valid_to > {timestamp}
|
||||
owners:
|
||||
- srose@mozilla.com
|
||||
labels:
|
||||
incremental: false
|
||||
schedule: daily
|
||||
scheduling:
|
||||
dag_name: bqetl_subplat
|
||||
# The whole table is overwritten every time, not a specific date partition.
|
||||
date_partition_parameter: null
|
||||
bigquery:
|
||||
time_partitioning:
|
||||
type: day
|
||||
field: valid_to
|
||||
require_partition_filter: false
|
||||
expiration_days: null
|
||||
clustering:
|
||||
fields:
|
||||
- valid_from
|
||||
references: {}
|
|
@ -0,0 +1,213 @@
|
|||
WITH history AS (
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.stripe_logical_subscriptions_history_v1`
|
||||
),
|
||||
countries AS (
|
||||
SELECT
|
||||
code,
|
||||
name
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.static.country_codes_v1`
|
||||
),
|
||||
customer_attribution_impressions AS (
|
||||
SELECT
|
||||
mozilla_account_id_sha256,
|
||||
impression_at,
|
||||
entrypoint,
|
||||
entrypoint_experiment,
|
||||
entrypoint_variation,
|
||||
utm_campaign,
|
||||
utm_content,
|
||||
utm_medium,
|
||||
utm_source,
|
||||
utm_term,
|
||||
service_ids
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.subplat_attribution_impressions_v1`
|
||||
CROSS JOIN
|
||||
UNNEST(mozilla_account_ids_sha256) AS mozilla_account_id_sha256
|
||||
UNION ALL
|
||||
-- Include historical VPN attributions from before VPN's SubPlat funnel was implemented on 2021-08-25.
|
||||
SELECT
|
||||
users.fxa_uid AS mozilla_account_id_sha256,
|
||||
users.created_at AS impression_at,
|
||||
CAST(NULL AS STRING) AS entrypoint,
|
||||
users_attribution.attribution.entrypoint_experiment,
|
||||
users_attribution.attribution.entrypoint_variation,
|
||||
users_attribution.attribution.utm_campaign,
|
||||
users_attribution.attribution.utm_content,
|
||||
users_attribution.attribution.utm_medium,
|
||||
users_attribution.attribution.utm_source,
|
||||
users_attribution.attribution.utm_term,
|
||||
['VPN'] AS service_ids
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.mozilla_vpn_derived.users_attribution_v1` AS users_attribution
|
||||
JOIN
|
||||
`moz-fx-data-shared-prod.mozilla_vpn_derived.users_v1` AS users
|
||||
ON
|
||||
users_attribution.user_id = users.id
|
||||
WHERE
|
||||
DATE(users.created_at) <= '2021-08-25'
|
||||
AND (
|
||||
users_attribution.attribution.entrypoint_experiment IS NOT NULL
|
||||
OR users_attribution.attribution.entrypoint_variation IS NOT NULL
|
||||
OR users_attribution.attribution.utm_campaign IS NOT NULL
|
||||
OR users_attribution.attribution.utm_content IS NOT NULL
|
||||
OR users_attribution.attribution.utm_medium IS NOT NULL
|
||||
OR users_attribution.attribution.utm_source IS NOT NULL
|
||||
OR users_attribution.attribution.utm_term IS NOT NULL
|
||||
)
|
||||
),
|
||||
subscription_starts AS (
|
||||
SELECT
|
||||
subscription.id AS subscription_id,
|
||||
subscription.started_at,
|
||||
subscription.mozilla_account_id_sha256,
|
||||
subscription.services
|
||||
FROM
|
||||
history
|
||||
QUALIFY
|
||||
1 = ROW_NUMBER() OVER (PARTITION BY subscription.id ORDER BY valid_from)
|
||||
),
|
||||
subscription_attributions AS (
|
||||
SELECT
|
||||
subscription_starts.subscription_id,
|
||||
MIN_BY(
|
||||
STRUCT(
|
||||
customer_attribution_impressions.impression_at,
|
||||
customer_attribution_impressions.entrypoint,
|
||||
customer_attribution_impressions.entrypoint_experiment,
|
||||
customer_attribution_impressions.entrypoint_variation,
|
||||
customer_attribution_impressions.utm_campaign,
|
||||
customer_attribution_impressions.utm_content,
|
||||
customer_attribution_impressions.utm_medium,
|
||||
customer_attribution_impressions.utm_source,
|
||||
customer_attribution_impressions.utm_term
|
||||
-- TODO: calculate normalized attribution values like `mozfun.norm.vpn_attribution()` does
|
||||
),
|
||||
customer_attribution_impressions.impression_at
|
||||
) AS first_touch_attribution,
|
||||
MAX_BY(
|
||||
STRUCT(
|
||||
customer_attribution_impressions.impression_at,
|
||||
customer_attribution_impressions.entrypoint,
|
||||
customer_attribution_impressions.entrypoint_experiment,
|
||||
customer_attribution_impressions.entrypoint_variation,
|
||||
customer_attribution_impressions.utm_campaign,
|
||||
customer_attribution_impressions.utm_content,
|
||||
customer_attribution_impressions.utm_medium,
|
||||
customer_attribution_impressions.utm_source,
|
||||
customer_attribution_impressions.utm_term
|
||||
-- TODO: calculate normalized attribution values like `mozfun.norm.vpn_attribution()` does
|
||||
),
|
||||
customer_attribution_impressions.impression_at
|
||||
) AS last_touch_attribution
|
||||
FROM
|
||||
subscription_starts
|
||||
CROSS JOIN
|
||||
UNNEST(subscription_starts.services) AS service
|
||||
JOIN
|
||||
customer_attribution_impressions
|
||||
ON
|
||||
subscription_starts.mozilla_account_id_sha256 = customer_attribution_impressions.mozilla_account_id_sha256
|
||||
AND service.id IN UNNEST(customer_attribution_impressions.service_ids)
|
||||
AND subscription_starts.started_at >= customer_attribution_impressions.impression_at
|
||||
GROUP BY
|
||||
subscription_id
|
||||
)
|
||||
SELECT
|
||||
history.id,
|
||||
history.valid_from,
|
||||
history.valid_to,
|
||||
history.provider_subscriptions_history_id,
|
||||
STRUCT(
|
||||
history.subscription.id,
|
||||
history.subscription.provider,
|
||||
history.subscription.payment_provider,
|
||||
history.subscription.provider_subscription_id,
|
||||
history.subscription.provider_subscription_item_id,
|
||||
history.subscription.provider_subscription_created_at,
|
||||
history.valid_from AS provider_subscription_updated_at,
|
||||
history.subscription.provider_customer_id,
|
||||
history.subscription.mozilla_account_id,
|
||||
history.subscription.mozilla_account_id_sha256,
|
||||
DENSE_RANK() OVER (
|
||||
PARTITION BY
|
||||
-- We don't have unhashed Mozilla Account IDs for some historical customers, so we use the hashed IDs instead,
|
||||
-- and if we don't have any Mozilla Account ID data we fall back to the provider's customer/subscription IDs.
|
||||
COALESCE(
|
||||
history.subscription.mozilla_account_id_sha256,
|
||||
history.subscription.provider_customer_id,
|
||||
history.subscription.provider_subscription_id
|
||||
)
|
||||
ORDER BY
|
||||
history.subscription.started_at,
|
||||
history.subscription.id
|
||||
) AS customer_subscription_number,
|
||||
history.subscription.country_code,
|
||||
COALESCE(countries.name, history.subscription.country_code, 'Unknown') AS country_name,
|
||||
history.subscription.services,
|
||||
history.subscription.provider_product_id,
|
||||
history.subscription.product_name,
|
||||
history.subscription.provider_plan_id,
|
||||
CONCAT(
|
||||
history.subscription.plan_interval_count,
|
||||
' ',
|
||||
history.subscription.plan_interval_type,
|
||||
IF(history.subscription.plan_interval_count > 1, 's', ''),
|
||||
IF(
|
||||
history.subscription.plan_amount IS NOT NULL,
|
||||
CONCAT(
|
||||
' ',
|
||||
history.subscription.plan_currency,
|
||||
' ',
|
||||
FORMAT('%.2f', history.subscription.plan_amount)
|
||||
),
|
||||
''
|
||||
),
|
||||
IF(history.subscription.is_bundle, ' bundle', '')
|
||||
) AS plan_summary,
|
||||
CONCAT(
|
||||
history.subscription.plan_interval_count,
|
||||
' ',
|
||||
history.subscription.plan_interval_type,
|
||||
IF(history.subscription.plan_interval_count > 1, 's', '')
|
||||
) AS plan_interval,
|
||||
history.subscription.plan_interval_type,
|
||||
history.subscription.plan_interval_count,
|
||||
CASE
|
||||
history.subscription.plan_interval_type
|
||||
WHEN 'month'
|
||||
THEN history.subscription.plan_interval_count
|
||||
WHEN 'year'
|
||||
THEN history.subscription.plan_interval_count * 12
|
||||
END AS plan_interval_months,
|
||||
history.subscription.plan_currency,
|
||||
history.subscription.plan_amount,
|
||||
history.subscription.is_bundle,
|
||||
history.subscription.is_trial,
|
||||
history.subscription.is_active,
|
||||
history.subscription.provider_status,
|
||||
history.subscription.started_at,
|
||||
history.subscription.ended_at,
|
||||
history.subscription.current_period_started_at,
|
||||
history.subscription.current_period_ends_at,
|
||||
history.subscription.auto_renew,
|
||||
history.subscription.auto_renew_disabled_at,
|
||||
history.subscription.has_refunds,
|
||||
history.subscription.has_fraudulent_charges,
|
||||
subscription_attributions.first_touch_attribution,
|
||||
subscription_attributions.last_touch_attribution
|
||||
) AS subscription
|
||||
FROM
|
||||
history
|
||||
LEFT JOIN
|
||||
countries
|
||||
ON
|
||||
history.subscription.country_code = countries.code
|
||||
LEFT JOIN
|
||||
subscription_attributions
|
||||
ON
|
||||
history.subscription.id = subscription_attributions.subscription_id
|
|
@ -0,0 +1,197 @@
|
|||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: valid_from
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: valid_to
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: provider_subscriptions_history_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: subscription
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: payment_provider
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_item_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_created_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_updated_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: provider_customer_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: mozilla_account_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: mozilla_account_id_sha256
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: customer_subscription_number
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: country_code
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: country_name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: services
|
||||
type: RECORD
|
||||
mode: REPEATED
|
||||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: tier
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_product_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: product_name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_plan_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_summary
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_type
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_count
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_months
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: plan_currency
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_amount
|
||||
type: NUMERIC
|
||||
mode: NULLABLE
|
||||
- name: is_bundle
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: is_trial
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: is_active
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: provider_status
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: started_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: ended_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: current_period_started_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: current_period_ends_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: auto_renew
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: auto_renew_disabled_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: has_refunds
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: has_fraudulent_charges
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: first_touch_attribution
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: impression_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: entrypoint
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_experiment
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_variation
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_campaign
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_content
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_medium
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_source
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_term
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: last_touch_attribution
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: impression_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: entrypoint
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_experiment
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_variation
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_campaign
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_content
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_medium
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_source
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_term
|
||||
type: STRING
|
||||
mode: NULLABLE
|
|
@ -0,0 +1,22 @@
|
|||
friendly_name: Monthly active logical subscriptions
|
||||
description: |-
|
||||
Monthly snapshots of logical subscriptions that were active at any point during each month.
|
||||
The latest state of the subscription during the month is saved.
|
||||
|
||||
Logical subscriptions are a continuous active period for a particular provider subscription.
|
||||
owners:
|
||||
- srose@mozilla.com
|
||||
labels:
|
||||
incremental: true
|
||||
schedule: daily
|
||||
scheduling:
|
||||
dag_name: bqetl_subplat
|
||||
date_partition_parameter: date
|
||||
bigquery:
|
||||
time_partitioning:
|
||||
type: month
|
||||
field: month_start_date
|
||||
require_partition_filter: false
|
||||
expiration_days: null
|
||||
clustering: null
|
||||
references: {}
|
|
@ -0,0 +1,59 @@
|
|||
WITH months AS (
|
||||
{% if is_init() %}
|
||||
SELECT
|
||||
month_start_date,
|
||||
LAST_DAY(month_start_date, MONTH) AS month_end_date
|
||||
FROM
|
||||
UNNEST(
|
||||
GENERATE_DATE_ARRAY(
|
||||
(
|
||||
SELECT
|
||||
DATE_TRUNC(DATE(MIN(started_at)), MONTH)
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform.logical_subscriptions`
|
||||
),
|
||||
CURRENT_DATE() - 1,
|
||||
INTERVAL 1 MONTH
|
||||
)
|
||||
) AS month_start_date
|
||||
{% else %}
|
||||
SELECT
|
||||
DATE_TRUNC(@date, MONTH) AS month_start_date,
|
||||
LAST_DAY(@date, MONTH) AS month_end_date
|
||||
{% endif %}
|
||||
),
|
||||
monthly_active_subscriptions AS (
|
||||
SELECT
|
||||
CONCAT(
|
||||
daily_subscriptions.subscription.id,
|
||||
'-',
|
||||
FORMAT_DATE('%Y-%m', months.month_start_date)
|
||||
) AS id,
|
||||
months.month_start_date,
|
||||
months.month_end_date,
|
||||
MIN_BY(daily_subscriptions, daily_subscriptions.date) AS earliest_daily_subscription,
|
||||
MAX_BY(daily_subscriptions, daily_subscriptions.date) AS latest_daily_subscription
|
||||
FROM
|
||||
months
|
||||
JOIN
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.daily_active_logical_subscriptions_v1` AS daily_subscriptions
|
||||
ON
|
||||
(daily_subscriptions.date BETWEEN months.month_start_date AND months.month_end_date)
|
||||
GROUP BY
|
||||
months.month_start_date,
|
||||
months.month_end_date,
|
||||
daily_subscriptions.subscription.id
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
month_start_date,
|
||||
month_end_date,
|
||||
latest_daily_subscription.logical_subscriptions_history_id,
|
||||
latest_daily_subscription.subscription,
|
||||
(
|
||||
earliest_daily_subscription.was_active_at_day_start
|
||||
AND earliest_daily_subscription.date = month_start_date
|
||||
) AS was_active_at_month_start,
|
||||
latest_daily_subscription.subscription.is_active AS was_active_at_month_end
|
||||
FROM
|
||||
monthly_active_subscriptions
|
|
@ -0,0 +1,203 @@
|
|||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: month_start_date
|
||||
type: DATE
|
||||
mode: NULLABLE
|
||||
- name: month_end_date
|
||||
type: DATE
|
||||
mode: NULLABLE
|
||||
- name: logical_subscriptions_history_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: subscription
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: payment_provider
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_item_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_created_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_updated_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: provider_customer_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: mozilla_account_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: mozilla_account_id_sha256
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: customer_subscription_number
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: country_code
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: country_name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: services
|
||||
type: RECORD
|
||||
mode: REPEATED
|
||||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: tier
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_product_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: product_name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_plan_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_summary
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_type
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_count
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_months
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: plan_currency
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_amount
|
||||
type: NUMERIC
|
||||
mode: NULLABLE
|
||||
- name: is_bundle
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: is_trial
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: is_active
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: provider_status
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: started_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: ended_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: current_period_started_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: current_period_ends_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: auto_renew
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: auto_renew_disabled_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: has_refunds
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: has_fraudulent_charges
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: first_touch_attribution
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: impression_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: entrypoint
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_experiment
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_variation
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_campaign
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_content
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_medium
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_source
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_term
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: last_touch_attribution
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: impression_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: entrypoint
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_experiment
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_variation
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_campaign
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_content
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_medium
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_source
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_term
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: was_active_at_month_start
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: was_active_at_month_end
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
|
@ -0,0 +1,16 @@
|
|||
friendly_name: Subscription services
|
||||
description: |-
|
||||
Metadata about subscription services and their tiers.
|
||||
owners:
|
||||
- srose@mozilla.com
|
||||
labels:
|
||||
incremental: false
|
||||
schedule: daily
|
||||
scheduling:
|
||||
dag_name: bqetl_subplat
|
||||
# The whole table is overwritten every time, not a specific date partition.
|
||||
date_partition_parameter: null
|
||||
bigquery:
|
||||
time_partitioning: null
|
||||
clustering: null
|
||||
references: {}
|
|
@ -0,0 +1,207 @@
|
|||
WITH services AS (
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
UNNEST(
|
||||
[
|
||||
STRUCT(
|
||||
'VPN' AS id,
|
||||
'Mozilla VPN' AS name,
|
||||
ARRAY<STRUCT<name STRING, subplat_capabilities ARRAY<STRING>>>[] AS tiers,
|
||||
['guardian_vpn_1'] AS subplat_capabilities,
|
||||
[STRUCT('e6eb0d1e856335fc' AS id, 'guardian-vpn' AS name)] AS subplat_oauth_clients
|
||||
),
|
||||
STRUCT(
|
||||
'FPN' AS id,
|
||||
'Firefox Private Network' AS name,
|
||||
ARRAY<STRUCT<name STRING, subplat_capabilities ARRAY<STRING>>>[] AS tiers,
|
||||
['fpn-browser'] AS subplat_capabilities,
|
||||
[STRUCT('565585c1745a144d' AS id, 'fx-priv-network' AS name)] AS subplat_oauth_clients
|
||||
),
|
||||
STRUCT(
|
||||
'Relay' AS id,
|
||||
'Relay Premium' AS name,
|
||||
[
|
||||
STRUCT('Email & Phone Masking' AS name, ['relay-phones'] AS subplat_capabilities),
|
||||
STRUCT('Email Masking' AS name, ['premium-relay'] AS subplat_capabilities)
|
||||
] AS tiers,
|
||||
['relay-phones', 'premium-relay'] AS subplat_capabilities,
|
||||
[STRUCT('9ebfe2c2f9ea3c58' AS id, 'fx-private-relay' AS name)] AS subplat_oauth_clients
|
||||
),
|
||||
STRUCT(
|
||||
'MDN' AS id,
|
||||
'MDN Plus' AS name,
|
||||
[
|
||||
STRUCT(
|
||||
'Supporter 10' AS name,
|
||||
['mdn_plus_10m', 'mdn_plus_10y'] AS subplat_capabilities
|
||||
),
|
||||
STRUCT('Plus 5' AS name, ['mdn_plus_5m', 'mdn_plus_5y'] AS subplat_capabilities)
|
||||
] AS tiers,
|
||||
[
|
||||
'mdn_plus',
|
||||
'mdn_plus_10m',
|
||||
'mdn_plus_10y',
|
||||
'mdn_plus_5m',
|
||||
'mdn_plus_5y'
|
||||
] AS subplat_capabilities,
|
||||
[STRUCT('720bc80adfa6988d' AS id, 'mdn-plus' AS name)] AS subplat_oauth_clients
|
||||
),
|
||||
STRUCT(
|
||||
'Hubs' AS id,
|
||||
'Hubs' AS name,
|
||||
[
|
||||
STRUCT('Professional' AS name, ['hubs-professional'] AS subplat_capabilities),
|
||||
STRUCT('Personal' AS name, ['managed-hubs'] AS subplat_capabilities)
|
||||
] AS tiers,
|
||||
['hubs-professional', 'managed-hubs'] AS subplat_capabilities,
|
||||
[
|
||||
STRUCT('8c3e3e6de4ee9731' AS id, 'mozilla-hubs' AS name),
|
||||
STRUCT('34bc0d0a6add7329' AS id, 'mozilla-hubs-dev' AS name)
|
||||
] AS subplat_oauth_clients
|
||||
)
|
||||
]
|
||||
)
|
||||
),
|
||||
stripe_products AS (
|
||||
SELECT
|
||||
id,
|
||||
PARSE_JSON(metadata) AS metadata
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.stripe_external.product_v1`
|
||||
),
|
||||
stripe_plans AS (
|
||||
SELECT
|
||||
id,
|
||||
PARSE_JSON(metadata) AS metadata,
|
||||
product_id
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.stripe_external.plan_v1`
|
||||
),
|
||||
service_stripe_product_ids AS (
|
||||
SELECT
|
||||
services.id AS service_id,
|
||||
ARRAY_AGG(DISTINCT stripe_products.id ORDER BY stripe_products.id) AS stripe_product_ids
|
||||
FROM
|
||||
services
|
||||
CROSS JOIN
|
||||
UNNEST(services.subplat_oauth_clients) AS subplat_oauth_client
|
||||
CROSS JOIN
|
||||
stripe_products
|
||||
JOIN
|
||||
UNNEST(
|
||||
SPLIT(JSON_VALUE(stripe_products.metadata['capabilities:' || subplat_oauth_client.id]), ',')
|
||||
) AS capability
|
||||
ON
|
||||
TRIM(capability) IN UNNEST(services.subplat_capabilities)
|
||||
GROUP BY
|
||||
service_id
|
||||
),
|
||||
service_stripe_plan_capabilities AS (
|
||||
SELECT DISTINCT
|
||||
services.id AS service_id,
|
||||
stripe_plans.id AS stripe_plan_id,
|
||||
TRIM(capability) AS capability
|
||||
FROM
|
||||
services
|
||||
CROSS JOIN
|
||||
UNNEST(services.subplat_oauth_clients) AS subplat_oauth_client
|
||||
CROSS JOIN
|
||||
stripe_plans
|
||||
LEFT JOIN
|
||||
stripe_products
|
||||
ON
|
||||
stripe_plans.product_id = stripe_products.id
|
||||
JOIN
|
||||
UNNEST(
|
||||
SPLIT(
|
||||
COALESCE(
|
||||
JSON_VALUE(stripe_plans.metadata['capabilities:' || subplat_oauth_client.id]),
|
||||
JSON_VALUE(stripe_products.metadata['capabilities:' || subplat_oauth_client.id])
|
||||
),
|
||||
','
|
||||
)
|
||||
) AS capability
|
||||
ON
|
||||
TRIM(capability) IN UNNEST(services.subplat_capabilities)
|
||||
),
|
||||
service_stripe_plan_ids AS (
|
||||
SELECT
|
||||
service_id,
|
||||
ARRAY_AGG(DISTINCT stripe_plan_id ORDER BY stripe_plan_id) AS stripe_plan_ids
|
||||
FROM
|
||||
service_stripe_plan_capabilities
|
||||
GROUP BY
|
||||
service_id
|
||||
),
|
||||
service_stripe_plan_tier_names AS (
|
||||
SELECT
|
||||
service_stripe_plan_capabilities.service_id,
|
||||
service_stripe_plan_capabilities.stripe_plan_id,
|
||||
-- Pick the first tier that matches (tiers should be in order of precedence).
|
||||
ARRAY_AGG(tier.name ORDER BY tier_order LIMIT 1)[ORDINAL(1)] tier_name
|
||||
FROM
|
||||
service_stripe_plan_capabilities
|
||||
JOIN
|
||||
services
|
||||
ON
|
||||
service_stripe_plan_capabilities.service_id = services.id
|
||||
JOIN
|
||||
UNNEST(services.tiers) AS tier
|
||||
WITH OFFSET AS tier_order
|
||||
ON
|
||||
service_stripe_plan_capabilities.capability IN UNNEST(tier.subplat_capabilities)
|
||||
GROUP BY
|
||||
service_id,
|
||||
stripe_plan_id
|
||||
),
|
||||
service_tier_stripe_plan_ids AS (
|
||||
SELECT
|
||||
service_id,
|
||||
tier_name,
|
||||
ARRAY_AGG(DISTINCT stripe_plan_id ORDER BY stripe_plan_id) AS stripe_plan_ids
|
||||
FROM
|
||||
service_stripe_plan_tier_names
|
||||
GROUP BY
|
||||
service_id,
|
||||
tier_name
|
||||
),
|
||||
service_tiers AS (
|
||||
SELECT
|
||||
services.id AS service_id,
|
||||
ARRAY_AGG(
|
||||
STRUCT(tier.name, tier.subplat_capabilities, service_tier_stripe_plan_ids.stripe_plan_ids)
|
||||
ORDER BY
|
||||
tier_order
|
||||
) AS tiers
|
||||
FROM
|
||||
services
|
||||
CROSS JOIN
|
||||
UNNEST(services.tiers) AS tier
|
||||
WITH OFFSET AS tier_order
|
||||
LEFT JOIN
|
||||
service_tier_stripe_plan_ids
|
||||
ON
|
||||
services.id = service_tier_stripe_plan_ids.service_id
|
||||
AND tier.name = service_tier_stripe_plan_ids.tier_name
|
||||
GROUP BY
|
||||
service_id
|
||||
)
|
||||
SELECT
|
||||
services.* REPLACE (service_tiers.tiers AS tiers),
|
||||
service_stripe_product_ids.stripe_product_ids,
|
||||
service_stripe_plan_ids.stripe_plan_ids
|
||||
FROM
|
||||
services
|
||||
LEFT JOIN
|
||||
service_tiers
|
||||
ON
|
||||
services.id = service_tiers.service_id
|
||||
LEFT JOIN
|
||||
service_stripe_product_ids
|
||||
ON
|
||||
services.id = service_stripe_product_ids.service_id
|
||||
LEFT JOIN
|
||||
service_stripe_plan_ids
|
||||
ON
|
||||
services.id = service_stripe_plan_ids.service_id
|
|
@ -0,0 +1,54 @@
|
|||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
description: Short, unique, human-readable ID for the service.
|
||||
- name: name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
description: Proper name for the service.
|
||||
- name: tiers
|
||||
type: RECORD
|
||||
mode: REPEATED
|
||||
description: Tiers for the service in order of precedence (i.e. highest tier first).
|
||||
fields:
|
||||
- name: name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
description: Human-readable name for the tier.
|
||||
- name: subplat_capabilities
|
||||
type: STRING
|
||||
mode: REPEATED
|
||||
description: SubPlat capabilities specific to the tier. Used to correlate with
|
||||
products/plans based on Stripe metadata.
|
||||
- name: stripe_plan_ids
|
||||
type: STRING
|
||||
mode: REPEATED
|
||||
description: IDs of Stripe plans with any of the tier's SubPlat capabilities.
|
||||
- name: subplat_capabilities
|
||||
type: STRING
|
||||
mode: REPEATED
|
||||
description: All SubPlat capabilities associated with the service. Used to correlate
|
||||
with products/plans based on Stripe metadata.
|
||||
- name: subplat_oauth_clients
|
||||
type: RECORD
|
||||
mode: REPEATED
|
||||
description: SubPlat OAuth clients associated with the service. Used to correlate
|
||||
with products/plans based on Stripe metadata, and SubPlat logs.
|
||||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
description: OAuth client ID.
|
||||
- name: name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
description: OAuth client name.
|
||||
- name: stripe_product_ids
|
||||
type: STRING
|
||||
mode: REPEATED
|
||||
description: IDs of Stripe products with any of the service's SubPlat capabilities.
|
||||
- name: stripe_plan_ids
|
||||
type: STRING
|
||||
mode: REPEATED
|
||||
description: IDs of Stripe plans with any of the service's SubPlat capabilities.
|
|
@ -0,0 +1,26 @@
|
|||
friendly_name: Stripe logical subscriptions history
|
||||
description: |-
|
||||
History of changes to Stripe logical subscriptions, which are a continuous active period for a particular subscription.
|
||||
|
||||
To get the historical state at a particular point in time use a condition like the following:
|
||||
valid_from <= {timestamp}
|
||||
AND valid_to > {timestamp}
|
||||
owners:
|
||||
- srose@mozilla.com
|
||||
labels:
|
||||
incremental: false
|
||||
schedule: daily
|
||||
scheduling:
|
||||
dag_name: bqetl_subplat
|
||||
# The whole table is overwritten every time, not a specific date partition.
|
||||
date_partition_parameter: null
|
||||
bigquery:
|
||||
time_partitioning:
|
||||
type: day
|
||||
field: valid_to
|
||||
require_partition_filter: false
|
||||
expiration_days: null
|
||||
clustering:
|
||||
fields:
|
||||
- valid_from
|
||||
references: {}
|
|
@ -0,0 +1,211 @@
|
|||
WITH subscriptions_history AS (
|
||||
SELECT
|
||||
id,
|
||||
valid_from,
|
||||
valid_to,
|
||||
subscription,
|
||||
customer,
|
||||
-- This should be kept in agreement with what SubPlat considers an active Stripe subscription.
|
||||
-- https://github.com/mozilla/fxa/blob/56026cd08e60525823c60c4f4116f705e79d6124/packages/fxa-shared/subscriptions/stripe.ts#L19-L24
|
||||
subscription.status IN ('active', 'past_due', 'trialing') AS subscription_is_active
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.stripe_subscriptions_history_v2`
|
||||
),
|
||||
active_subscriptions_history AS (
|
||||
-- Only include a subscription's history once it becomes active.
|
||||
SELECT
|
||||
*,
|
||||
FIRST_VALUE(
|
||||
IF(subscription_is_active, valid_from, NULL) IGNORE NULLS
|
||||
) OVER subscription_history_to_date_asc AS subscription_first_active_at
|
||||
FROM
|
||||
subscriptions_history
|
||||
QUALIFY
|
||||
LOGICAL_OR(subscription_is_active) OVER subscription_history_to_date_asc
|
||||
WINDOW
|
||||
subscription_history_to_date_asc AS (
|
||||
PARTITION BY
|
||||
subscription.id
|
||||
ORDER BY
|
||||
valid_from
|
||||
ROWS BETWEEN
|
||||
UNBOUNDED PRECEDING
|
||||
AND CURRENT ROW
|
||||
)
|
||||
),
|
||||
plan_services AS (
|
||||
SELECT
|
||||
plan_id,
|
||||
ARRAY_AGG(
|
||||
STRUCT(services.id, services.name, tier.name AS tier)
|
||||
ORDER BY
|
||||
services.id
|
||||
) AS services
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.services_v1` AS services
|
||||
CROSS JOIN
|
||||
UNNEST(services.stripe_plan_ids) AS plan_id
|
||||
LEFT JOIN
|
||||
UNNEST(services.tiers) AS tier
|
||||
ON
|
||||
plan_id IN UNNEST(tier.stripe_plan_ids)
|
||||
GROUP BY
|
||||
plan_id
|
||||
),
|
||||
paypal_subscriptions AS (
|
||||
SELECT DISTINCT
|
||||
subscription_id
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.stripe_external.invoice_v1`
|
||||
WHERE
|
||||
JSON_VALUE(metadata, '$.paypalTransactionId') IS NOT NULL
|
||||
),
|
||||
subscriptions_history_charge_summaries AS (
|
||||
SELECT
|
||||
history.id AS subscriptions_history_id,
|
||||
ARRAY_AGG(
|
||||
cards.country IGNORE NULLS
|
||||
ORDER BY
|
||||
-- Prefer charges that succeeded.
|
||||
IF(charges.status = 'succeeded', 1, 2),
|
||||
charges.created DESC
|
||||
LIMIT
|
||||
1
|
||||
)[SAFE_ORDINAL(1)] AS latest_card_country,
|
||||
LOGICAL_OR(refunds.status = 'succeeded') AS has_refunds,
|
||||
LOGICAL_OR(
|
||||
charges.fraud_details_user_report = 'fraudulent'
|
||||
OR (
|
||||
charges.fraud_details_stripe_report = 'fraudulent'
|
||||
AND charges.fraud_details_user_report IS DISTINCT FROM 'safe'
|
||||
)
|
||||
OR (refunds.reason = 'fraudulent' AND refunds.status = 'succeeded')
|
||||
) AS has_fraudulent_charges
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.stripe_external.charge_v1` AS charges
|
||||
JOIN
|
||||
`moz-fx-data-shared-prod.stripe_external.invoice_v1` AS invoices
|
||||
ON
|
||||
charges.invoice_id = invoices.id
|
||||
JOIN
|
||||
active_subscriptions_history AS history
|
||||
ON
|
||||
invoices.subscription_id = history.subscription.id
|
||||
AND charges.created < history.valid_to
|
||||
LEFT JOIN
|
||||
`moz-fx-data-shared-prod.stripe_external.card_v1` AS cards
|
||||
ON
|
||||
charges.card_id = cards.id
|
||||
LEFT JOIN
|
||||
`moz-fx-data-shared-prod.stripe_external.refund_v1` AS refunds
|
||||
ON
|
||||
charges.id = refunds.charge_id
|
||||
GROUP BY
|
||||
subscriptions_history_id
|
||||
)
|
||||
SELECT
|
||||
CONCAT(
|
||||
'Stripe-',
|
||||
history.subscription.id,
|
||||
'-',
|
||||
FORMAT_TIMESTAMP('%FT%H:%M:%E6S', history.subscription_first_active_at),
|
||||
'-',
|
||||
FORMAT_TIMESTAMP('%FT%H:%M:%E6S', history.valid_from)
|
||||
) AS id,
|
||||
history.valid_from,
|
||||
history.valid_to,
|
||||
history.id AS provider_subscriptions_history_id,
|
||||
STRUCT(
|
||||
CONCAT(
|
||||
'Stripe-',
|
||||
history.subscription.id,
|
||||
'-',
|
||||
FORMAT_TIMESTAMP('%FT%H:%M:%E6S', history.subscription_first_active_at)
|
||||
) AS id,
|
||||
'Stripe' AS provider,
|
||||
IF(paypal_subscriptions.subscription_id IS NOT NULL, 'PayPal', 'Stripe') AS payment_provider,
|
||||
history.subscription.id AS provider_subscription_id,
|
||||
subscription_item.id AS provider_subscription_item_id,
|
||||
history.subscription.created AS provider_subscription_created_at,
|
||||
history.subscription.customer_id AS provider_customer_id,
|
||||
history.customer.metadata.userid AS mozilla_account_id,
|
||||
history.customer.metadata.userid_sha256 AS mozilla_account_id_sha256,
|
||||
CASE
|
||||
-- Use the same address hierarchy as Stripe Tax after we enabled Stripe Tax (FXA-5457).
|
||||
-- https://stripe.com/docs/tax/customer-locations#address-hierarchy
|
||||
WHEN DATE(history.valid_to) >= '2022-12-01'
|
||||
AND (
|
||||
DATE(history.subscription.ended_at) >= '2022-12-01'
|
||||
OR history.subscription.ended_at IS NULL
|
||||
)
|
||||
THEN COALESCE(
|
||||
NULLIF(history.customer.shipping.address.country, ''),
|
||||
NULLIF(history.customer.address.country, ''),
|
||||
charge_summaries.latest_card_country
|
||||
)
|
||||
-- SubPlat copies the PayPal billing agreement country to the customer's address.
|
||||
WHEN paypal_subscriptions.subscription_id IS NOT NULL
|
||||
THEN NULLIF(history.customer.address.country, '')
|
||||
ELSE charge_summaries.latest_card_country
|
||||
END AS country_code,
|
||||
plan_services.services,
|
||||
subscription_item.plan.product.id AS provider_product_id,
|
||||
subscription_item.plan.product.name AS product_name,
|
||||
subscription_item.plan.id AS provider_plan_id,
|
||||
subscription_item.plan.`interval` AS plan_interval_type,
|
||||
subscription_item.plan.interval_count AS plan_interval_count,
|
||||
UPPER(subscription_item.plan.currency) AS plan_currency,
|
||||
(CAST(subscription_item.plan.amount AS DECIMAL) / 100) AS plan_amount,
|
||||
IF(ARRAY_LENGTH(plan_services.services) > 1, TRUE, FALSE) AS is_bundle,
|
||||
IF(
|
||||
history.subscription.status = 'trialing'
|
||||
OR (
|
||||
history.subscription.ended_at
|
||||
BETWEEN history.subscription.trial_start
|
||||
AND history.subscription.trial_end
|
||||
),
|
||||
TRUE,
|
||||
FALSE
|
||||
) AS is_trial,
|
||||
history.subscription_is_active AS is_active,
|
||||
history.subscription.status AS provider_status,
|
||||
history.subscription_first_active_at AS started_at,
|
||||
history.subscription.ended_at,
|
||||
-- TODO: ended_reason
|
||||
IF(
|
||||
history.subscription.ended_at IS NULL,
|
||||
history.subscription.current_period_start,
|
||||
NULL
|
||||
) AS current_period_started_at,
|
||||
IF(
|
||||
history.subscription.ended_at IS NULL,
|
||||
history.subscription.current_period_end,
|
||||
NULL
|
||||
) AS current_period_ends_at,
|
||||
history.subscription.cancel_at_period_end IS NOT TRUE AS auto_renew,
|
||||
IF(
|
||||
history.subscription.cancel_at_period_end,
|
||||
history.subscription.canceled_at,
|
||||
NULL
|
||||
) AS auto_renew_disabled_at,
|
||||
-- TODO: promotion_codes
|
||||
-- TODO: promotion_discounts_amount
|
||||
COALESCE(charge_summaries.has_refunds, FALSE) AS has_refunds,
|
||||
COALESCE(charge_summaries.has_fraudulent_charges, FALSE) AS has_fraudulent_charges
|
||||
) AS subscription
|
||||
FROM
|
||||
active_subscriptions_history AS history
|
||||
CROSS JOIN
|
||||
UNNEST(history.subscription.items) AS subscription_item
|
||||
LEFT JOIN
|
||||
plan_services
|
||||
ON
|
||||
subscription_item.plan.id = plan_services.plan_id
|
||||
LEFT JOIN
|
||||
paypal_subscriptions
|
||||
ON
|
||||
history.subscription.id = paypal_subscriptions.subscription_id
|
||||
LEFT JOIN
|
||||
subscriptions_history_charge_summaries AS charge_summaries
|
||||
ON
|
||||
history.id = charge_summaries.subscriptions_history_id
|
|
@ -0,0 +1,117 @@
|
|||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: valid_from
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: valid_to
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: provider_subscriptions_history_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: subscription
|
||||
type: RECORD
|
||||
mode: NULLABLE
|
||||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: payment_provider
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_item_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_subscription_created_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: provider_customer_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: mozilla_account_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: mozilla_account_id_sha256
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: country_code
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: services
|
||||
type: RECORD
|
||||
mode: REPEATED
|
||||
fields:
|
||||
- name: id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: tier
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_product_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: product_name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: provider_plan_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_type
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_interval_count
|
||||
type: INTEGER
|
||||
mode: NULLABLE
|
||||
- name: plan_currency
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_amount
|
||||
type: NUMERIC
|
||||
mode: NULLABLE
|
||||
- name: is_bundle
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: is_trial
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: is_active
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: provider_status
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: started_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: ended_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: current_period_started_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: current_period_ends_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: auto_renew
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: auto_renew_disabled_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: has_refunds
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
||||
- name: has_fraudulent_charges
|
||||
type: BOOLEAN
|
||||
mode: NULLABLE
|
|
@ -223,6 +223,7 @@ questionable_subscription_plans_history AS (
|
|||
plans.trial_period_days,
|
||||
plans.usage_type
|
||||
) AS plan,
|
||||
ROW_NUMBER() OVER subscription_plan_changes_asc AS subscription_plan_number,
|
||||
LAG(plan_changes.plan_id) OVER subscription_plan_changes_asc AS previous_plan_id,
|
||||
plan_changes.subscription_plan_start AS valid_from,
|
||||
COALESCE(
|
||||
|
@ -265,8 +266,7 @@ synthetic_subscription_start_changelog AS (
|
|||
questionable_subscription_plans_history AS plans_history
|
||||
ON
|
||||
changelog.subscription.id = plans_history.subscription_id
|
||||
AND changelog.subscription.start_date >= plans_history.valid_from
|
||||
AND changelog.subscription.start_date < plans_history.valid_to
|
||||
AND plans_history.subscription_plan_number = 1
|
||||
),
|
||||
synthetic_plan_change_changelog AS (
|
||||
SELECT
|
||||
|
@ -287,7 +287,8 @@ synthetic_plan_change_changelog AS (
|
|||
ON
|
||||
plans_history.subscription_id = changelog.subscription.id
|
||||
WHERE
|
||||
plans_history.valid_from > changelog.subscription.start_date
|
||||
plans_history.subscription_plan_number > 1
|
||||
AND plans_history.valid_from > changelog.subscription.start_date
|
||||
),
|
||||
synthetic_trial_start_changelog AS (
|
||||
SELECT
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
friendly_name: SubPlat attribution impressions
|
||||
description: |-
|
||||
Subscription service attribution impressions from SubPlat logs.
|
||||
owners:
|
||||
- srose@mozilla.com
|
||||
labels:
|
||||
incremental: true
|
||||
schedule: daily
|
||||
scheduling:
|
||||
dag_name: bqetl_subplat
|
||||
depends_on_past: true
|
||||
# The whole table is overwritten every time, not a specific date partition.
|
||||
date_partition_parameter: null
|
||||
parameters:
|
||||
- 'date:DATE:{{ds}}'
|
||||
bigquery:
|
||||
time_partitioning:
|
||||
type: day
|
||||
field: impression_at
|
||||
require_partition_filter: false
|
||||
expiration_days: null
|
||||
clustering:
|
||||
fields:
|
||||
- flow_id
|
||||
references: {}
|
|
@ -0,0 +1,158 @@
|
|||
WITH services AS (
|
||||
SELECT
|
||||
id,
|
||||
ARRAY(SELECT id FROM UNNEST(subplat_oauth_clients)) AS subplat_oauth_client_ids,
|
||||
ARRAY(SELECT name FROM UNNEST(subplat_oauth_clients)) AS subplat_oauth_client_names,
|
||||
stripe_product_ids,
|
||||
stripe_plan_ids
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.services_v1`
|
||||
),
|
||||
new_service_flow_events AS (
|
||||
SELECT
|
||||
events.flow_id,
|
||||
MIN(events.event_time) OVER (PARTITION BY events.flow_id) AS flow_min_event_time,
|
||||
events.event_time,
|
||||
events.event_type,
|
||||
events.entrypoint,
|
||||
events.entrypoint_experiment,
|
||||
events.entrypoint_variation,
|
||||
events.utm_campaign,
|
||||
events.utm_content,
|
||||
events.utm_medium,
|
||||
events.utm_source,
|
||||
events.utm_term,
|
||||
events.mozilla_account_id_sha256,
|
||||
events.oauth_client_id,
|
||||
events.oauth_client_name,
|
||||
events.product_id,
|
||||
events.plan_id,
|
||||
services.id AS service_id
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.subplat_flow_events_v1` AS events
|
||||
-- If multiple services match this join it can cause fan-outs, but the way we aggregate things this doesn't matter.
|
||||
LEFT JOIN
|
||||
services
|
||||
ON
|
||||
events.oauth_client_id IN UNNEST(services.subplat_oauth_client_ids)
|
||||
OR events.oauth_client_name IN UNNEST(services.subplat_oauth_client_names)
|
||||
-- For a while Bedrock incorrectly passed VPN's OAuth client name as the OAuth client ID.
|
||||
OR events.oauth_client_id IN UNNEST(services.subplat_oauth_client_names)
|
||||
OR events.product_id IN UNNEST(services.stripe_product_ids)
|
||||
OR events.plan_id IN UNNEST(services.stripe_plan_ids)
|
||||
WHERE
|
||||
{% if is_init() %}
|
||||
DATE(events.log_timestamp) < CURRENT_DATE()
|
||||
{% else %}
|
||||
-- Reprocess the previous day's events as well in case a flow spanned multiple days
|
||||
-- but wasn't saved here on the initial day due to not having attribution values yet.
|
||||
(DATE(events.log_timestamp) BETWEEN (@date - 1) AND @date)
|
||||
{% endif %}
|
||||
),
|
||||
service_flow_events AS (
|
||||
{% if is_init() %}
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
new_service_flow_events
|
||||
{% else %}
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
new_service_flow_events
|
||||
UNION ALL
|
||||
SELECT
|
||||
flow_id,
|
||||
flow_started_at AS flow_min_event_time,
|
||||
impression_at AS event_time,
|
||||
event_type,
|
||||
entrypoint,
|
||||
entrypoint_experiment,
|
||||
entrypoint_variation,
|
||||
utm_campaign,
|
||||
utm_content,
|
||||
utm_medium,
|
||||
utm_source,
|
||||
utm_term,
|
||||
mozilla_account_id_sha256,
|
||||
oauth_client_id,
|
||||
oauth_client_name,
|
||||
product_id,
|
||||
plan_id,
|
||||
service_id
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.subplat_attribution_impressions_v1`
|
||||
-- Unnesting these arrays can cause fan-outs, but the way we reaggregate things this doesn't matter.
|
||||
LEFT JOIN
|
||||
UNNEST(mozilla_account_ids_sha256) AS mozilla_account_id_sha256
|
||||
LEFT JOIN
|
||||
UNNEST(oauth_client_ids) AS oauth_client_id
|
||||
LEFT JOIN
|
||||
UNNEST(oauth_client_names) AS oauth_client_name
|
||||
LEFT JOIN
|
||||
UNNEST(product_ids) AS product_id
|
||||
LEFT JOIN
|
||||
UNNEST(plan_ids) AS plan_id
|
||||
LEFT JOIN
|
||||
UNNEST(service_ids) AS service_id
|
||||
{% endif %}
|
||||
)
|
||||
SELECT
|
||||
flow_id,
|
||||
MIN(flow_min_event_time) AS flow_started_at,
|
||||
ARRAY_AGG(
|
||||
IF(
|
||||
entrypoint_experiment IS NOT NULL
|
||||
OR entrypoint_variation IS NOT NULL
|
||||
OR utm_campaign IS NOT NULL
|
||||
OR utm_content IS NOT NULL
|
||||
OR utm_medium IS NOT NULL
|
||||
OR utm_source IS NOT NULL
|
||||
OR utm_term IS NOT NULL,
|
||||
STRUCT(
|
||||
event_time AS impression_at,
|
||||
event_type,
|
||||
entrypoint,
|
||||
entrypoint_experiment,
|
||||
entrypoint_variation,
|
||||
utm_campaign,
|
||||
utm_content,
|
||||
utm_medium,
|
||||
utm_source,
|
||||
utm_term
|
||||
),
|
||||
NULL
|
||||
) IGNORE NULLS
|
||||
ORDER BY
|
||||
event_time
|
||||
LIMIT
|
||||
1
|
||||
)[ORDINAL(1)].*,
|
||||
ARRAY_AGG(
|
||||
DISTINCT mozilla_account_id_sha256 IGNORE NULLS
|
||||
ORDER BY
|
||||
mozilla_account_id_sha256
|
||||
) AS mozilla_account_ids_sha256,
|
||||
ARRAY_AGG(DISTINCT oauth_client_id IGNORE NULLS ORDER BY oauth_client_id) AS oauth_client_ids,
|
||||
ARRAY_AGG(
|
||||
DISTINCT oauth_client_name IGNORE NULLS
|
||||
ORDER BY
|
||||
oauth_client_name
|
||||
) AS oauth_client_names,
|
||||
ARRAY_AGG(DISTINCT product_id IGNORE NULLS ORDER BY product_id) AS product_ids,
|
||||
ARRAY_AGG(DISTINCT plan_id IGNORE NULLS ORDER BY plan_id) AS plan_ids,
|
||||
ARRAY_AGG(DISTINCT service_id IGNORE NULLS ORDER BY service_id) AS service_ids
|
||||
FROM
|
||||
service_flow_events AS events
|
||||
GROUP BY
|
||||
flow_id
|
||||
HAVING
|
||||
LOGICAL_OR(
|
||||
events.entrypoint_experiment IS NOT NULL
|
||||
OR events.entrypoint_variation IS NOT NULL
|
||||
OR events.utm_campaign IS NOT NULL
|
||||
OR events.utm_content IS NOT NULL
|
||||
OR events.utm_medium IS NOT NULL
|
||||
OR events.utm_source IS NOT NULL
|
||||
OR events.utm_term IS NOT NULL
|
||||
)
|
|
@ -0,0 +1,55 @@
|
|||
fields:
|
||||
- name: flow_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: flow_started_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: impression_at
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: event_type
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_experiment
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_variation
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_campaign
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_content
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_medium
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_source
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_term
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: mozilla_account_ids_sha256
|
||||
type: STRING
|
||||
mode: REPEATED
|
||||
- name: oauth_client_ids
|
||||
type: STRING
|
||||
mode: REPEATED
|
||||
- name: oauth_client_names
|
||||
type: STRING
|
||||
mode: REPEATED
|
||||
- name: product_ids
|
||||
type: STRING
|
||||
mode: REPEATED
|
||||
- name: plan_ids
|
||||
type: STRING
|
||||
mode: REPEATED
|
||||
- name: service_ids
|
||||
type: STRING
|
||||
mode: REPEATED
|
|
@ -0,0 +1,22 @@
|
|||
friendly_name: SubPlat flow events
|
||||
description: |-
|
||||
Subscription service flow events from SubPlat logs.
|
||||
owners:
|
||||
- srose@mozilla.com
|
||||
labels:
|
||||
incremental: true
|
||||
schedule: daily
|
||||
scheduling:
|
||||
dag_name: bqetl_subplat
|
||||
depends_on_past: true
|
||||
date_partition_parameter: date
|
||||
bigquery:
|
||||
time_partitioning:
|
||||
type: day
|
||||
field: log_timestamp
|
||||
require_partition_filter: false
|
||||
expiration_days: null
|
||||
clustering:
|
||||
fields:
|
||||
- flow_id
|
||||
references: {}
|
|
@ -0,0 +1,84 @@
|
|||
WITH new_flow_events AS (
|
||||
SELECT
|
||||
logger,
|
||||
`timestamp` AS log_timestamp,
|
||||
COALESCE(event_time, `timestamp`) AS event_time,
|
||||
event_type,
|
||||
flow_id,
|
||||
user_id AS mozilla_account_id_sha256,
|
||||
oauth_client_id,
|
||||
service AS oauth_client_name,
|
||||
checkout_type,
|
||||
payment_provider,
|
||||
subscription_id,
|
||||
product_id,
|
||||
plan_id,
|
||||
entrypoint,
|
||||
entrypoint_experiment,
|
||||
entrypoint_variation,
|
||||
utm_campaign,
|
||||
utm_content,
|
||||
utm_medium,
|
||||
utm_source,
|
||||
utm_term,
|
||||
promotion_code,
|
||||
country_code_source,
|
||||
country_code,
|
||||
country,
|
||||
`language`,
|
||||
os_name,
|
||||
os_version,
|
||||
ua_browser,
|
||||
ua_version,
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.firefox_accounts.fxa_all_events`
|
||||
WHERE
|
||||
fxa_log IN ('content', 'auth', 'stdout')
|
||||
AND flow_id IS NOT NULL
|
||||
{% if is_init() %}
|
||||
AND DATE(`timestamp`) < CURRENT_DATE()
|
||||
{% else %}
|
||||
AND DATE(`timestamp`) = @date
|
||||
{% endif %}
|
||||
),
|
||||
services_metadata AS (
|
||||
SELECT
|
||||
ARRAY_AGG(DISTINCT subplat_oauth_client.id IGNORE NULLS) AS subplat_oauth_client_ids,
|
||||
ARRAY_AGG(DISTINCT subplat_oauth_client.name IGNORE NULLS) AS subplat_oauth_client_names
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.services_v1`
|
||||
LEFT JOIN
|
||||
UNNEST(subplat_oauth_clients) AS subplat_oauth_client
|
||||
),
|
||||
existing_flow_ids AS (
|
||||
{% if is_init() %}
|
||||
SELECT
|
||||
CAST(NULL AS STRING) AS flow_id
|
||||
{% else %}
|
||||
SELECT DISTINCT
|
||||
flow_id
|
||||
FROM
|
||||
`moz-fx-data-shared-prod.subscription_platform_derived.subplat_flow_events_v1`
|
||||
{% endif %}
|
||||
)
|
||||
SELECT
|
||||
new_flow_events.*
|
||||
FROM
|
||||
new_flow_events
|
||||
CROSS JOIN
|
||||
services_metadata
|
||||
LEFT JOIN
|
||||
existing_flow_ids
|
||||
ON
|
||||
new_flow_events.flow_id = existing_flow_ids.flow_id
|
||||
QUALIFY
|
||||
LOGICAL_OR(
|
||||
new_flow_events.oauth_client_id IN UNNEST(services_metadata.subplat_oauth_client_ids)
|
||||
OR new_flow_events.oauth_client_name IN UNNEST(services_metadata.subplat_oauth_client_names)
|
||||
-- For a while Bedrock incorrectly passed VPN's OAuth client name as the OAuth client ID.
|
||||
OR new_flow_events.oauth_client_id IN UNNEST(services_metadata.subplat_oauth_client_names)
|
||||
OR new_flow_events.subscription_id IS NOT NULL
|
||||
OR new_flow_events.product_id IS NOT NULL
|
||||
OR new_flow_events.plan_id IS NOT NULL
|
||||
) OVER (PARTITION BY new_flow_events.flow_id)
|
||||
OR existing_flow_ids.flow_id IS NOT NULL
|
|
@ -0,0 +1,91 @@
|
|||
fields:
|
||||
- name: logger
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: log_timestamp
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: event_time
|
||||
type: TIMESTAMP
|
||||
mode: NULLABLE
|
||||
- name: event_type
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: flow_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: mozilla_account_id_sha256
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: oauth_client_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: oauth_client_name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: checkout_type
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: payment_provider
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: subscription_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: product_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: plan_id
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_experiment
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: entrypoint_variation
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_campaign
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_content
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_medium
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_source
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: utm_term
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: promotion_code
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: country_code_source
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: country_code
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: country
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: language
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: os_name
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: os_version
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: ua_browser
|
||||
type: STRING
|
||||
mode: NULLABLE
|
||||
- name: ua_version
|
||||
type: STRING
|
||||
mode: NULLABLE
|
Загрузка…
Ссылка в новой задаче