-
Notifications
You must be signed in to change notification settings - Fork 150
feat(DENG-11237): export referral totals CSV to GCS #9738
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
dc0695d
feat(DENG-11237): add Firefox Referral Program install-counts pipeline
phil-lee70 6debfae
fix(DENG-11237): add all-time partition filter to referral totals query
phil-lee70 124e1d8
fix(DENG-11237): add schema.yaml for referral GCS-extract task
phil-lee70 9a305ea
refactor(DENG-11237): hold GCS export out of this PR
phil-lee70 8dd8d51
fix(DENG-11237): represent attribution_ext JSON fixture as inline object
phil-lee70 53cd8e8
feat(DENG-11237): export referral totals CSV to GCS
phil-lee70 eb3dc5e
fix(DENG-11237): address review on referral GCS export
phil-lee70 44091d9
fix(DENG-11237): drop schema.yaml + destination_table:null for GCS ex…
phil-lee70 9c52ab2
Merge branch 'main' into deng-11237-referral-gcs-export
phil-lee70 b047d17
fix(DENG-11237): restore schema.yaml for GCS export (stage-deploy nee…
phil-lee70 54296cd
chore(DENG-11237): drop stale checks.sql from GCS export branch
phil-lee70 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| """Export referral install totals from BigQuery to a CSV file in GCS. | ||
|
|
||
| Supports the Firefox Referral Program (DENG-11237). The Website team syncs the | ||
| CSV into the Postgres DB behind the referral hub page on firefox.com. | ||
|
|
||
| Output format matches the Website team's request: `invite_code,total_installs` | ||
| (no header by default), one file per run date named `referral_data-<date>.csv`. | ||
| The run date (not wall-clock time) is used so an Airflow retry overwrites the | ||
| same object rather than accumulating a second file for the same logical date. | ||
| """ | ||
|
|
||
| import logging | ||
|
|
||
| import rich_click as click | ||
| from google.cloud import bigquery | ||
|
|
||
| logging.basicConfig(level=logging.INFO) | ||
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| @click.command() | ||
| @click.option( | ||
| "--source-project", | ||
| required=True, | ||
| help="Google Cloud Project where the source table is located.", | ||
| ) | ||
| @click.option( | ||
| "--source-dataset", | ||
| required=True, | ||
| help="Dataset in BigQuery where the source table is located.", | ||
| ) | ||
| @click.option( | ||
| "--source-table", | ||
| required=True, | ||
| help="Name of the source table in BigQuery.", | ||
| ) | ||
| @click.option( | ||
| "--destination-bucket", | ||
| required=True, | ||
| help="Destination Google Cloud Storage bucket (name only, no gs:// prefix).", | ||
| ) | ||
| @click.option( | ||
| "--destination-prefix", | ||
| required=False, | ||
| default="", | ||
| help="Optional prefix (subfolder) within the bucket. Omit to write to the " | ||
| "bucket root.", | ||
| ) | ||
| @click.option( | ||
| "--date", | ||
| required=True, | ||
| help="Run date (YYYY-MM-DD), used in the output filename. Pass Airflow's " | ||
| "{{ds}} so retries overwrite the same object instead of accumulating files.", | ||
| ) | ||
| @click.option( | ||
| "--include-header/--no-include-header", | ||
| default=False, | ||
| help="Whether to write a CSV header row. Defaults to no header per the " | ||
| "Website team's requested format.", | ||
| ) | ||
| def export_referral_totals_to_gcs( | ||
| source_project: str, | ||
| source_dataset: str, | ||
| source_table: str, | ||
| destination_bucket: str, | ||
| destination_prefix: str, | ||
| date: str, | ||
| include_header: bool, | ||
| ): | ||
| """Extract the referral totals table to a CSV file in GCS, named by run date.""" | ||
| client = bigquery.Client(source_project) | ||
|
|
||
| filename = f"referral_data-{date}.csv" | ||
| object_path = ( | ||
| f"{destination_prefix.rstrip('/')}/{filename}" | ||
| if destination_prefix | ||
| else filename | ||
| ) | ||
| destination_uri = f"gs://{destination_bucket}/{object_path}" | ||
|
|
||
| job_config = bigquery.job.ExtractJobConfig( | ||
| destination_format=bigquery.job.DestinationFormat.CSV, | ||
| print_header=include_header, | ||
| ) | ||
|
|
||
| extract_job = client.extract_table( | ||
| source=f"{source_project}.{source_dataset}.{source_table}", | ||
| destination_uris=[destination_uri], | ||
| job_config=job_config, | ||
| ) | ||
| try: | ||
| extract_job.result() # Waits for the job to complete. | ||
| except Exception as e: | ||
| raise click.ClickException( | ||
| f"Export to {destination_uri} failed: {extract_job.errors}" | ||
| ) from e | ||
|
|
||
| log.info(f"Export successful: {destination_uri}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
38 changes: 38 additions & 0 deletions
38
...ata-shared-prod/firefox_referral_derived/referral_installs_totals_to_gcs_v1/metadata.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| friendly_name: Referral Installs Totals to GCS | ||
| description: |- | ||
| Extracts referral_installs_totals_v1 to a CSV file in the Website team's GCS | ||
| bucket for the Firefox Referral Program (DENG-11237). firefox.com syncs the | ||
| CSV into the Postgres DB behind the referral hub page. | ||
|
|
||
| CSV column contract (no header row), written to the bucket root: | ||
| invite_code STRING referral (invite) code, fxrefer: prefix stripped | ||
| total_installs INTEGER cumulative all-time distinct first_run installs | ||
| One file per run date named `referral_data-<date>.csv` (date = Airflow {{ds}}), | ||
| so an Airflow retry overwrites the same object. Runs daily. | ||
|
|
||
| This task only extracts to GCS; it does not logically build a queryable table. | ||
| A schema.yaml is shipped alongside because the --isolated stage-deploy CI | ||
| hard-fails to resolve a query.py artifact without one; a side effect is an | ||
| empty companion table (see schema.yaml for why destination_table: null can't | ||
| be used here). | ||
|
|
||
| Destination bucket `fx-referral-data-prod` (project moz-fx-springfield-prod) is | ||
| owned by the Website team; the Airflow workload service account | ||
| (default-workloads@moz-fx-data-airflow-gke-prod.iam.gserviceaccount.com) was | ||
| granted write access via mozilla/webservices-infra#11912. | ||
| owners: | ||
| - phlee@mozilla.com | ||
| labels: | ||
| incremental: false | ||
| owner1: phlee | ||
| scheduling: | ||
| dag_name: bqetl_firefox_referral | ||
| arguments: | ||
| - --source-project=moz-fx-data-shared-prod | ||
| - --source-dataset=firefox_referral_derived | ||
| - --source-table=referral_installs_totals_v1 | ||
| - --destination-bucket=fx-referral-data-prod | ||
| - --date={{ds}} | ||
| referenced_tables: | ||
| - ['moz-fx-data-shared-prod', 'firefox_referral_derived', 'referral_installs_totals_v1'] | ||
| bigquery: null |
6 changes: 6 additions & 0 deletions
6
...-fx-data-shared-prod/firefox_referral_derived/referral_installs_totals_to_gcs_v1/query.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| """Export referral install totals to a CSV file in the Website team's GCS bucket.""" | ||
|
|
||
| from bigquery_etl.referral_export import export_referral_totals_to_gcs | ||
|
|
||
| if __name__ == "__main__": | ||
| export_referral_totals_to_gcs() |
27 changes: 27 additions & 0 deletions
27
...-data-shared-prod/firefox_referral_derived/referral_installs_totals_to_gcs_v1/schema.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # This task extracts referral_installs_totals_v1 to a CSV in GCS; it does not | ||
| # logically build a queryable table. This schema.yaml is required so the | ||
| # --isolated stage-deploy CI can resolve the query.py artifact: without it, the | ||
| # resolver ladder hard-fails (a .py can't be dry-run, and SELECT */get_table on | ||
| # the not-yet-existent table are denied), which fails the deploy rather than | ||
| # skipping it. | ||
| # | ||
| # Tradeoff: because a schema resolves, deploy also creates an (empty, unused) | ||
| # companion table `firefox_referral_derived.referral_installs_totals_to_gcs_v1`. | ||
| # The clean way to suppress that is scheduling.destination_table: null, but that | ||
| # is incompatible with a query.py task — Task.of_python_script sets | ||
| # query_file_path after construction, so the __attrs_post_init__ check | ||
| # ("One of destination_table or query_file_path must be specified") fails DAG | ||
| # generation. Columns mirror referral_installs_totals_v1 and document the CSV. | ||
| fields: | ||
| - name: invite_code | ||
| type: STRING | ||
| mode: NULLABLE | ||
| description: >- | ||
| Referral (invite) code, `fxrefer:` prefix stripped (expected 17 chars). | ||
| First column of the exported CSV. | ||
| - name: total_installs | ||
| type: INTEGER | ||
| mode: NULLABLE | ||
| description: >- | ||
| Cumulative all-time distinct Firefox first_run installs for this invite | ||
| code. Second column of the exported CSV. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The date is off by one: the DAG runs 0 4 * * *, so {{ds}} is the previous day. The file written on Aug 6 is named referral_data-2026-08-05.csv. If the Website team's job looks for today's date it might not find any file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is expected and intentional. {{ds}} is the logical date (interval start), so the 0 4 * * * run on Aug 6 writes referral_data-2026-08-05.csv — a day behind wall-clock. This keeps the export idempotent: a retry or backfill for a logical date overwrites the same file rather than creating a new one.