【发布时间】:2021-05-07 12:59:57
【问题描述】:
我是一名初级开发人员,负责将 Facebook API 实施到现有项目。但是,业务团队发现 BigQuery 上显示的 Google Analytics 结果是错误的。他们要求我修复它。这是架构:
我所做的是:
-
在 BigQuery 上,检查来自 Google Analytics(分析)的结果有多近/多远。我发现有一个模式,我在 BigQuery 上得到的结果总是 GA 原始值的 1、2 或 3 倍。
-
我检查了 Compute Engine 上是否真的有多个 cron 作业。实际上只有 1 个 cron 作业并且每天运行一次。
-
我在 Google Cloud Storage 上验证了结果。 Google Cloud Storage 上的结果是正确的,如下所示:
根据这些信息,我坚信问题出在云功能上,因为它是 GCS 和 BQ 之间的唯一元素。我查看了从 GCS 触发文件的 Cloud Function,但找不到任何重复的操作。
你知道我怎样才能找到问题吗?
云功能
BUCKET = "xxxx"
GOOGLE_PROJECT = "xxxx"
HEADER_MAPPING = {
"Source/Medium": "source_medium",
"Campaign": "campaign",
"Last Non-Direct Click Conversions": "last_non_direct_click_conversions",
"Last Non-Direct Click Conversion Value": "last_non_direct_click_conversion_value",
"Last Click Prio Conversions": "last_click_prio_conversions",
"Last Click Prio Conversion Value": "last_click_prio_conversion_value",
"Data-Driven Conversions": "dda_conversions",
"Data-Driven Conversion Value": "dda_conversion_value",
"% Change in Conversions from Last Non-Direct Click to Last Click Prio": "last_click_prio_vs_last_click",
"% Change in Conversions from Last Non-Direct Click to Data-Driven": "dda_vs_last_click"
}
SPEND_HEADER_MAPPING = {
"Source/Medium": "source_medium",
"Campaign": "campaign",
"Spend": "spend"
}
tables_schema = {
"google-analytics": [
bigquery.SchemaField("date", bigquery.enums.SqlTypeNames.DATE, mode='REQUIRED'),
bigquery.SchemaField("week", bigquery.enums.SqlTypeNames.INT64, mode='REQUIRED'),
bigquery.SchemaField("goal", bigquery.enums.SqlTypeNames.STRING, mode='REQUIRED'),
bigquery.SchemaField("source", bigquery.enums.SqlTypeNames.STRING, mode='NULLABLE'),
bigquery.SchemaField("medium", bigquery.enums.SqlTypeNames.STRING, mode='NULLABLE'),
bigquery.SchemaField("campaign", bigquery.enums.SqlTypeNames.STRING, mode='NULLABLE'),
bigquery.SchemaField("last_non_direct_click_conversions", bigquery.enums.SqlTypeNames.INT64, mode='NULLABLE'),
bigquery.SchemaField("last_non_direct_click_conversion_value", bigquery.enums.SqlTypeNames.FLOAT64, mode='NULLABLE'),
bigquery.SchemaField("last_click_prio_conversions", bigquery.enums.SqlTypeNames.INT64, mode='NULLABLE'),
bigquery.SchemaField("last_click_prio_conversion_value", bigquery.enums.SqlTypeNames.FLOAT64, mode='NULLABLE'),
bigquery.SchemaField("dda_conversions", bigquery.enums.SqlTypeNames.FLOAT64, mode='NULLABLE'),
bigquery.SchemaField("dda_conversion_value", bigquery.enums.SqlTypeNames.FLOAT64, mode='NULLABLE'),
bigquery.SchemaField("last_click_prio_vs_last_click", bigquery.enums.SqlTypeNames.FLOAT64, mode='NULLABLE'),
bigquery.SchemaField("dda_vs_last_click", bigquery.enums.SqlTypeNames.FLOAT64, mode='NULLABLE')
],
"google-analytics-spend": [
bigquery.SchemaField("date", bigquery.enums.SqlTypeNames.DATE, mode='REQUIRED'),
bigquery.SchemaField("week", bigquery.enums.SqlTypeNames.INT64, mode='REQUIRED'),
bigquery.SchemaField("source", bigquery.enums.SqlTypeNames.STRING, mode='NULLABLE'),
bigquery.SchemaField("medium", bigquery.enums.SqlTypeNames.STRING, mode='NULLABLE'),
bigquery.SchemaField("campaign", bigquery.enums.SqlTypeNames.STRING, mode='NULLABLE'),
bigquery.SchemaField("spend", bigquery.enums.SqlTypeNames.FLOAT64, mode='NULLABLE'),
]
}
def download_from_gcs(file):
client = storage.Client()
bucket = client.get_bucket(BUCKET)
blob = bucket.get_blob(file['name'])
file_name = os.path.basename(os.path.normpath(file['name']))
blob.download_to_filename(f"/tmp/{file_name}")
return file_name
def load_in_bigquery(file_object, dataset: str, table: str):
client = bigquery.Client()
table_id = f"{GOOGLE_PROJECT}.{dataset}.{table}"
job_config = bigquery.LoadJobConfig(
source_format=bigquery.SourceFormat.CSV,
skip_leading_rows=1,
autodetect=True,
schema=tables_schema[table]
)
job = client.load_table_from_file(file_object, table_id, job_config=job_config)
job.result() # Wait for the job to complete.
def __order_columns(df: pd.DataFrame, spend=False) ->pd.DataFrame:
# We want to have source and medium columns at the third position
# for a spend data frame and at the fourth postion for others df
# because spend data frame don't have goal column.
pos = 2 if spend else 3
cols = df.columns.tolist()
cols[pos:2] = cols[-2:]
cols = cols[:-2]
return df[cols]
def __common_transformation(df: pd.DataFrame, date: str, goal: str) -> pd.DataFrame:
# for any kind of dataframe, we add date and week columns
# based on the file name and we split Source/Medium from the csv
# into two different columns
week_of_the_year = datetime.strptime(date, '%Y-%m-%d').isocalendar()[1]
df.insert(0, 'date', date)
df.insert(1, 'week', week_of_the_year)
mapping = SPEND_HEADER_MAPPING if goal == "spend" else HEADER_MAPPING
print(df.columns.tolist())
df = df.rename(columns=mapping)
print(df.columns.tolist())
print(df)
df["source_medium"] = df["source_medium"].str.replace(' ', '')
df[["source", "medium"]] = df["source_medium"].str.split('/', expand=True)
df = df.drop(["source_medium"], axis=1)
df["week"] = df["week"].astype(int, copy=False)
return df
def __transform_spend(df: pd.DataFrame) -> pd.DataFrame:
df["spend"] = df["spend"].astype(float, copy=False)
df = __order_columns(df, spend=True)
return df[df.columns[:6]]
def __transform_attribution(df: pd.DataFrame, goal: str) -> pd.DataFrame:
df.insert(2, 'goal', goal)
df["last_non_direct_click_conversions"] = df["last_non_direct_click_conversions"].astype(int, copy=False)
df["last_click_prio_conversions"] = df["last_click_prio_conversions"].astype(int, copy=False)
df["dda_conversions"] = df["dda_conversions"].astype(float, copy=False)
return __order_columns(df)
def transform(df: pd.DataFrame, file_name) -> pd.DataFrame:
goal, date, *_ = file_name.split('_')
df = __common_transformation(df, date, goal)
# we only add goal in attribution df (google-analytics table).
return __transform_spend(df) if "spend" in file_name else __transform_attribution(df, goal)
def main(event, context):
"""Triggered by a change to a Cloud Storage bucket.
Args:
event (dict): Event payload.
context (google.cloud.functions.Context): Metadata for the event.
"""
file = event
file_name = download_from_gcs(file)
df = pd.read_csv(f"/tmp/{file_name}")
transformed_df = transform(df, file_name)
with open(f"/tmp/bq_{file_name}", "w") as file_object:
file_object.write(transformed_df.to_csv(index=False))
with open(f"/tmp/bq_{file_name}", "rb") as file_object:
table = "google-analytics-spend" if "spend" in file_name else "google-analytics"
load_in_bigquery(file_object, dataset='attribution', table=table)
更新
是的,云函数由 GCS 对象 finalize 事件触发。此外,该功能不会在失败时自动重试。
我正在遵循您的建议,现在我正在查看我的 Cloud Function 页面上的日志表。在最后 10 行日志数据中,似乎运行了 3 个不同的 Cloud Function 实例。展开每一行时,我无法获得更多详细信息。
我现在还要检查 BigQuery 日志。我想最简单的解决方案是使用BigQueryAuditMetadata 并获取有关表何时更新的日志?
【问题讨论】:
-
"当我展开每一行时,我无法获得更多细节。"我在您的代码中看到了一些“打印”功能。您可以使用它(至少用于调试,临时),并在任何地方添加这些“打印”。例如 - 获取和打印关于对象的元数据信息,这应该被处理。例如,您有一个“事件”有效负载。您可以使用“event['crc32c']”、“event['md5Hash']”、“event['timeCreated']”并将它们打印(到日志中)。
-
就 BigQuery 作业历史记录而言 - 只需转到 BigQuery 控制台 UI,然后选择“作业历史记录”标签 - 并在那里查看。请记住 - 该项目可能基于您的代码 - 应该是部署云功能的位置,因为看起来该工作应该在该项目中。这不是你的个人工作。
-
现在我很确定有多个云函数调用。我尝试制作一个 .txt 文件来跟踪已上传到 BigQuery 的文件。我正在考虑添加一个 if/else 来检查要上传的文件是否已经存在于该列表中。如果列表中存在 .txt 文件,我会跳过。否则,我会将文件上传到 BQ 并将文件名写入列表。但这不起作用,因为它不可能在云函数上编写。您还有其他建议吗?
-
我现在有点忙,但我会在今天晚些时候回来更新我的答案。很快 - 该函数是幂等的,并且进程的状态(如果数据/文件是否上传到 BQ)应该保留在云函数之外。文本文件(在某些 GCS 存储桶中)是一种选择,但 GCS 在这种特殊情况下有很多缺点。 Firestore - 是更好的选择。
标签: google-cloud-platform google-bigquery google-cloud-functions google-cloud-storage