كيفية تحميل البيانات الأولية من إعلانات Google إلى Google BigQuery

نشرت: 2022-04-12

من خلال تحليل فعالية حملات إعلانات Google الإعلانية في Google Analytics ، قد تواجه أخذ العينات أو تجميع البيانات أو غيرها من قيود واجهة النظام. لحسن الحظ ، يمكن حل هذه المشكلة بسهولة عن طريق تحميل البيانات الأولية من خدمة الإعلانات الخاصة بك إلى Google BigQuery.

في هذه المقالة ، ستتعلم كيفية تحميل البيانات الأولية من حساب إعلانات Google الخاص بك إلى BigQuery وتحديد جميع علامات UTM للحملات ذات التصنيف التلقائي.

أنت بحاجة إلى OWOX BI لربط معلومات الحملة بنشاط المستخدم على الموقع. اشترك للحصول على عرض توضيحي وسنشرح بالتفصيل جميع التحديات التي يمكنك حلها باستخدام OWOX BI.

سجل للحصول على نسخة تجريبية

جدول المحتويات

  • لماذا تحتاج إلى بيانات أولية من إعلانات Google
  • طريقتان لتحميل البيانات الأولية من إعلانات Google إلى BigQuery
  • كيفية تكوين التحميل باستخدام نقل البيانات
  • كيفية إعداد التحميل باستخدام Ads Script
  • كيفية توصيل تنزيل البيانات من إعلانات Google بـ OWOX BI
  • نصائح مفيدة

اكتشف القيمة الحقيقية للحملات

استيراد بيانات التكلفة تلقائيًا إلى Google Analytics من جميع خدماتك الإعلانية. قارن تكاليف الحملة وتكلفة النقرة وعائد النفقات الإعلانية في تقرير واحد.

ابدأ النسخه التجريبيه

لماذا تحتاج إلى بيانات أولية من إعلانات Google

ستسمح لك البيانات الأولية من إعلانات Google بتحليل الحملات الإعلانية بدقة وصولاً إلى كل كلمة رئيسية. من خلال تحميل البيانات إلى BigQuery ، يمكنك:

  • أنشئ تقارير مفصلة كما تريد دون التقيد بقيود GA.
  • تحديد فعالية الحملات الإعلانية على مستوى الجلسة والمستخدم.
  • احسب ROI و ROAS و CRR حسب المنطقة ونوع المستخدم (الجديد أو المُعاد) والجهاز وأي معلمة أخرى.
  • إدارة أسعارك بشكل فعال وإنشاء قوائم تجديد النشاط التسويقي.
  • اجمع البيانات من إعلانات Google و Google Analytics و CRM لتقييم فعالية الحملات بناءً على الهامش وإمكانية استرداد عناصرك.
  • تدريب نموذج ML الخاص بك للحصول على تخطيط أكثر دقة.

لفهم الحملات والإعلانات والكلمات الرئيسية التي تجذب المستخدمين إلى موقعك ، تحتاج إلى دمج البيانات من إعلانات Google و Analytics في BigQuery. يمكنك القيام بذلك باستخدام دفق OWOX BI.

يرسل دفق هذه المعلومات بيانات سلوك المستخدم غير المستندة إلى عينات على موقعك إلى GBQ. يتم إرسال الزيارات في الوقت الفعلي ، ثم يتم تشكيل الجلسات بناءً على هذه النتائج.

جرب OWOX BI مجانًا

يتم الحصول على معلومات مصدر حركة مرور OWOX BI من العلامات الإعلانية لعلامات UTM. العلامات يدوية وتلقائية.

لنفترض أنك قمت بتمييز الإعلان يدويًا وحصلت على عنوان URL هذا:

https://example.com/؟utm_source=facebook&utm_medium=cpc&utm_campaign=utm_tags

في هذه الحالة ، بعد توصيل OWOX BI ، ستتوفر لديك بيانات المصدر والقناة والحملة في جدول GBQ:

  • trafficSource.source - جوجل
  • trafficSource.medium - cpc
  • trafficSource.campaign - utm_tags

اقرأ المزيد حول كيفية إنشاء علامات UTM بشكل صحيح.

إذا قمت بتمكين الترميز التلقائي في الخدمة الإعلانية ، فسيتم تعيين معلمة gclid خاصة لكل إعلان من إعلاناتك. تتم إضافته إلى عنوان URL للصفحة المقصودة عندما ينقر المستخدم على الإعلان.

مثال على هذا الارتباط:

http://www.example.com/؟gclid=TeSter-123

إذا كنت تستخدم ترميزًا تلقائيًا ، فلا يمكنك الحصول على مصدر أو وسيط أو حملة من gclid بدون بيانات أولية - ستكون هذه الحقول فارغة في جداول BigQuery التي يجمعها OWOX BI.

ماذا يمكنك أن تفعل في مثل هذه الحالة وكيف يمكنك الحصول على أسماء الحملات والمعلمات الأخرى ، مع وجود gclid فقط؟ تكوين التحميل التلقائي من إعلانات Google إلى GBQ.

ملاحظة: إذا لم يتم وضع علامة على الإعلان على الإطلاق ، فسيقوم OWOX BI بتعيين الرابط على النحو التالي:

  • لمصادر غير Google كزيارات إحالة (مثل facebook / إحالة)
  • لمصدر Google كزيارات مباشرة (مباشرة / لا شيء)

إذا كان هناك الكثير من الزيارات المباشرة / لا شيء في تقاريرك ، فقد لا يكون لديك تصفية الروبوت ممكّنة أو قد يكون لديك عدد كبير من الإعلانات غير المميزة.

طريقتان لتحميل البيانات الأولية من إعلانات Google إلى BigQuery

نحن نستخدم طريقتين ونوصي بهما لتحميل البيانات الأولية من إعلانات Google: Data Transfer Connector و Ads Script.

الطريقة التي تختارها تعتمد على أهدافك وميزانيتك.

ميزات نقل البيانات

  • التكامل الأصلي مع GBQ.
  • قم بتنزيل البيانات التاريخية لأي فترة دون قيود.
  • شحن مجاني.

ميزات برنامج إعلانات Google النصي

  • حر.
  • لا يمكنك تحميل البيانات التاريخية. يتم تنزيل معلومات اليوم السابق فقط.
  • يتطلب المزيد إذا كنت تريد إعداد التحميل من عدد كبير من الحسابات. ستحتاج إلى إجراء تغييرات يدويًا على البرنامج النصي لكل حساب. في الوقت نفسه ، هناك مخاطر عالية لارتكاب خطأ.

ما تحتاجه للإعدادات

المشاريع والحسابات النشطة في:

  • Google Cloud Platform (GCP)
  • Google BigQuery (GBQ)
  • OWOX BI
  • إعلانات جوجل

التمكن من:

  • مالك في GCP
  • المسؤول في GBQ
  • التحرير في OWOX BI. هام: يمكن فقط للمستخدم الذي أنشأ Google Analytics ← خط أنابيب BigQuery المتدفق من Google تشغيل التنزيل من إعلانات Google.
  • القراءة في إعلانات جوجل

كيفية منح حقوق الوصول في GBQ

افتح وحدة تحكم GCP وحدد IAM والمسؤول - إدارة الموارد من القائمة الجانبية. ثم حدد المشروع وانقر فوق إضافة عضو. أدخل البريد الإلكتروني للمستخدم ، وحدد دور مسؤول BigQuery ، واحفظ التغييرات.

الوصول إلى BigQuery

كيفية تكوين التحميل باستخدام نقل البيانات

الخطوة 1. أنشئ مشروعًا في Google Cloud Platform

إذا كان لديك بالفعل مشروع في برنامج "شركاء Google المعتمدون" ، فتخط هذه الخطوة. إذا لم يكن كذلك ، فافتح وحدة تحكم GCP وحدد IAM والمسؤول - إدارة الموارد من القائمة الجانبية. انقر فوق الزر "إنشاء مشروع". ثم أدخل اسم المشروع ، وحدد المؤسسة ، وانقر فوق إنشاء:

Google Cloud Platform - إعداد مشروع

تأكد من تمكين الفواتير. للقيام بذلك ، افتح علامة التبويب الفوترة - إدارة الحساب في القائمة الجانبية ، وحدد المشروع ، ثم اربط حساب الفوترة:

الفوترة في Google Cloud Platform

بعد ذلك ، أكمل جميع الحقول عن طريق إدخال جهات الاتصال وتفاصيل بطاقة الدفع. إذا كان هذا هو مشروعك الأول في برنامج "شركاء Google المعتمدون" ، فستتلقى 300 دولار أمريكي يمكن استخدامها لمدة 12 شهرًا. المشاريع التي لها حساب أو حسابان على "إعلانات Google" وما يصل إلى 100000 مستخدم فريد شهريًا سيكون لها ما يكفي لمدة عام. عندما تستنفد هذا الحد ، لن تحتاج إلى إعادة الأموال. لمزيد من الاستخدام ، ما عليك سوى إعادة ملء الرصيد الموجود على البطاقة التي قمت بربطها بالمشروع.

الخطوة 2. قم بتشغيل API BigQuery

بعد إنشاء مشروع ، يجب عليك تنشيط BigQuery API. للقيام بذلك ، انتقل إلى APIs & Services - Dashboard من القائمة الجانبية لـ GCP ، وحدد المشروع ، وانقر فوق تمكين واجهات برمجة التطبيقات والخدمات:

تفعيل BigQuery API

في مكتبة واجهة برمجة التطبيقات ، ابحث عن "BigQuery API" وانقر على تمكين:

تفعيل BigQuery لواجهة برمجة التطبيقات

لاستخدام API ، انقر فوق إنشاء بيانات الاعتماد:

إنشاء بيانات الاعتماد

من القائمة المنسدلة ، اختر BigQuery API وانقر على ما بيانات الاعتماد التي أحتاجها؟

اختيار أوراق الاعتماد

أنشئ اسم حساب الخدمة وحدد مستوى الوصول إلى دور BigQuery. حدد نوع مفتاح JSON وانقر فوق متابعة:

حدد بيانات الاعتماد

الخطوة 3. تفعيل API لنقل البيانات

بعد ذلك ، تحتاج إلى تنشيط خدمة البيانات في BigQuery. للقيام بذلك ، افتح GBQ وحدد التحويلات من القائمة الجانبية على اليسار. ثم قم بتمكين BigQuery Data Transfer API:

تفعيل BigQuery Data Transfer API

الخطوة 4. قم بإعداد مجموعة البيانات في GBQ

في BigQuery ، حدد المشروع وانقر على الزر "إنشاء مجموعة بيانات" على اليسار. أكمل جميع الحقول المطلوبة لمجموعة البيانات الجديدة (الاسم والموقع والاحتفاظ):

قم بإنشاء مجموعة البيانات في GBQ

الخطوة 5. إعداد نقل البيانات من إعلانات Google

انقر فوق علامة التبويب Transfers في القائمة الجانبية ، ثم انقر فوق Create Transfer. بعد ذلك ، حدد إعلانات Google (AdWords سابقًا) للمصدر وأدخل اسم التحميل ، على سبيل المثال ، نقل البيانات.

ضمن خيارات الجدولة ، يمكنك ترك الإعداد الافتراضي على البدء الآن أو تعيين التاريخ والوقت اللذين تريدهما لبدء التنزيل. في حقل التكرارات ، حدد عدد مرات التحميل: يوميًا ، أسبوعيًا ، شهريًا عند الطلب ، إلخ.

إعدادات نقل البيانات

ثم يتعين عليك تحديد مجموعة بيانات GBQ لتحميل التقارير من إعلانات Google إليها. أدخل الرقم التعريفي للعميل (هذا هو معرف حساب إعلانات Google أو الرقم التعريفي لمركز عملائي) وانقر فوق إضافة. يمكنك عرض الرقم التعريفي للعميل في حساب إعلانات Google الخاص بك في الزاوية العلوية اليسرى بجوار بريدك الإلكتروني.

حدد إعدادات نقل البيانات

ثم تحتاج إلى تفويض حساب Gmail الذي تستخدمه. في اليوم التالي ، ستظهر المعلومات في مجموعة البيانات التي حددتها عند إعداد النقل.

نتيجة لذلك ، ستتلقى كمية كبيرة من البيانات الأولية في GBQ التي يمكنك العمل بها: جداول حسب الحملات ، والجماهير ، والجداول العامة (المخصصة) ، والكلمات الرئيسية ، والتحويلات. على سبيل المثال ، إذا كنت تريد إنشاء لوحة معلومات مخصصة ، فيمكنك سحب البيانات غير المجمعة من هذه الجداول.

كيفية إعداد التحميل باستخدام Ads Script

افتح حساب إعلانات Google الخاص بك ، وانقر فوق الأدوات والإعدادات في الزاوية اليمنى العليا ، وحدد الإجراءات المجمعة - البرامج النصية ، وانقر فوق رمز Plus:

نص برمجي في إعلانات Google

بعد ذلك ، في الزاوية العلوية اليمنى ، انقر على زر واجهات برمجة التطبيقات المتقدمة ، وحدد BigQuery ، واحفظ تغييراتك:

حفظ التغييرات

تأكد من التسجيل باستخدام الحساب الذي سجلت الدخول إلى إعلانات Google به:

إذن إعلانات Google

انسخ البرنامج النصي أدناه. في BIGQUERY_PROJECT_ID و BIGQUERY_DATASET_ID وأسطر بريدك الإلكتروني ، استبدل القيم بمعلوماتك الخاصة: اسم المشروع ومجموعة بيانات GBQ والبريد الإلكتروني. الصق نص البرنامج النصي في محرر النص.

    /** * @name Export Data to BigQuery * * @overview The Export Data to BigQuery script sets up a BigQuery * dataset and tables, downloads a report from AdWords and then * loads the report to BigQuery. * * @author AdWords Scripts Team [[email protected]] * * @version 1.3 */ var CONFIG = { BIGQUERY_PROJECT_ID: 'BQ project name', BIGQUERY_DATASET_ID: AdWordsApp.currentAccount().getCustomerId().replace(/-/g, '_'), // Truncate existing data, otherwise will append. TRUNCATE_EXISTING_DATASET: false, TRUNCATE_EXISTING_TABLES: true, // Lists of reports and fields to retrieve from AdWords. REPORTS: [], RECIPIENT_EMAILS: [ 'Your email' ] }; var report = { NAME: 'CLICK_PERFORMANCE_REPORT', //https://developers.google.com/adwords/api/docs/appendix/reports/click-performance-report CONDITIONS: '', FIELDS: {'AccountDescriptiveName': 'STRING', 'AdFormat': 'STRING', 'AdGroupId': 'STRING', 'AdGroupName': 'STRING', 'AoiCountryCriteriaId': 'STRING', 'CampaignId': 'STRING', 'CampaignLocationTargetId': 'STRING', 'CampaignName': 'STRING', 'CampaignStatus': 'STRING', 'Clicks': 'INTEGER', 'ClickType': 'STRING', 'CreativeId': 'STRING', 'CriteriaId': 'STRING', 'CriteriaParameters': 'STRING', 'Date': 'DATE', 'Device': 'STRING', 'ExternalCustomerId': 'STRING', 'GclId': 'STRING', 'KeywordMatchType': 'STRING', 'LopCountryCriteriaId': 'STRING', 'Page': 'INTEGER' }, DATE_RANGE: new Date(new Date().setDate(new Date().getDate()-1)).toISOString().slice(0, 10).replace(/-/g, "")+','+new Date(new Date().setDate(new Date().getDate()-1)).toISOString().slice(0, 10).replace(/-/g, ""), DATE: new Date(new Date().setDate(new Date().getDate()-1)).toISOString().slice(0, 10).replace(/-/g, "") }; //Regular export CONFIG.REPORTS.push(JSON.parse(JSON.stringify(report))); //One-time historical export //for(var i=2;i<91;i++){ // report.DATE_RANGE = new Date(new Date().setDate(new Date().getDate()-i)).toISOString().slice(0, 10).replace(/-/g, "")+','+new Date(new Date().setDate(new Date().getDate()-i)).toISOString().slice(0, 10).replace(/-/g, ""); // report.DATE = new Date(new Date().setDate(new Date().getDate()-i)).toISOString().slice(0, 10).replace(/-/g, ""); // CONFIG.REPORTS.push(JSON.parse(JSON.stringify(report))); //} /** * Main method */ function main() { createDataset(); for (var i = 0; i < CONFIG.REPORTS.length; i++) { var reportConfig = CONFIG.REPORTS[i]; createTable(reportConfig); } var jobIds = processReports(); waitTillJobsComplete(jobIds); sendEmail(jobIds); } /** * Creates a new dataset. * * If a dataset with the same id already exists and the truncate flag * is set, will truncate the old dataset. If the truncate flag is not * set, then will not create a new dataset. */ function createDataset() { if (datasetExists()) { if (CONFIG.TRUNCATE_EXISTING_DATASET) { BigQuery.Datasets.remove(CONFIG.BIGQUERY_PROJECT_ID, CONFIG.BIGQUERY_DATASET_ID, {'deleteContents' : true}); Logger.log('Truncated dataset.'); } else { Logger.log('Dataset %s already exists. Will not recreate.', CONFIG.BIGQUERY_DATASET_ID); return; } } // Create new dataset. var dataSet = BigQuery.newDataset(); dataSet.friendlyName = CONFIG.BIGQUERY_DATASET_ID; dataSet.datasetReference = BigQuery.newDatasetReference(); dataSet.datasetReference.projectId = CONFIG.BIGQUERY_PROJECT_ID; dataSet.datasetReference.datasetId = CONFIG.BIGQUERY_DATASET_ID; dataSet = BigQuery.Datasets.insert(dataSet, CONFIG.BIGQUERY_PROJECT_ID); Logger.log('Created dataset with id %s.', dataSet.id); } /** * Checks if dataset already exists in project. * * @return {boolean} Returns true if dataset already exists. */ function datasetExists() { // Get a list of all datasets in project. var datasets = BigQuery.Datasets.list(CONFIG.BIGQUERY_PROJECT_ID); var datasetExists = false; // Iterate through each dataset and check for an id match. if (datasets.datasets != null) { for (var i = 0; i < datasets.datasets.length; i++) { var dataset = datasets.datasets[i]; if (dataset.datasetReference.datasetId == CONFIG.BIGQUERY_DATASET_ID) { datasetExists = true; break; } } } return datasetExists; } /** * Creates a new table. * * If a table with the same id already exists and the truncate flag * is set, will truncate the old table. If the truncate flag is not * set, then will not create a new table. * * @param {Object} reportConfig Report configuration including report name, * conditions, and fields. */ function createTable(reportConfig) { var tableName = reportConfig.NAME+reportConfig.DATE; if (tableExists(tableName)) { if (CONFIG.TRUNCATE_EXISTING_TABLES) { BigQuery.Tables.remove(CONFIG.BIGQUERY_PROJECT_ID, CONFIG.BIGQUERY_DATASET_ID, tableName); Logger.log('Truncated table %s.', tableName); } else { Logger.log('Table %s already exists. Will not recreate.', tableName); return; } } // Create new table. var table = BigQuery.newTable(); var schema = BigQuery.newTableSchema(); var bigQueryFields = []; // Add each field to table schema. var fieldNames = Object.keys(reportConfig.FIELDS); for (var i = 0; i < fieldNames.length; i++) { var fieldName = fieldNames[i]; var bigQueryFieldSchema = BigQuery.newTableFieldSchema(); bigQueryFieldSchema.description = fieldName; bigQueryFieldSchema.name = fieldName; bigQueryFieldSchema.type = reportConfig.FIELDS[fieldName]; bigQueryFields.push(bigQueryFieldSchema); } schema.fields = bigQueryFields; table.schema = schema; table.friendlyName = tableName; table.tableReference = BigQuery.newTableReference(); table.tableReference.datasetId = CONFIG.BIGQUERY_DATASET_ID; table.tableReference.projectId = CONFIG.BIGQUERY_PROJECT_ID; table.tableReference.tableId = tableName; table = BigQuery.Tables.insert(table, CONFIG.BIGQUERY_PROJECT_ID, CONFIG.BIGQUERY_DATASET_ID); Logger.log('Created table with id %s.', table.id); } /** * Checks if table already exists in dataset. * * @param {string} tableId The table id to check existence. * * @return {boolean} Returns true if table already exists. */ function tableExists(tableId) { // Get a list of all tables in the dataset. var tables = BigQuery.Tables.list(CONFIG.BIGQUERY_PROJECT_ID, CONFIG.BIGQUERY_DATASET_ID); var tableExists = false; // Iterate through each table and check for an id match. if (tables.tables != null) { for (var i = 0; i < tables.tables.length; i++) { var table = tables.tables[i]; if (table.tableReference.tableId == tableId) { tableExists = true; break; } } } return tableExists; } /** * Process all configured reports * * Iterates through each report to: retrieve AdWords data, * backup data to Drive (if configured), load data to BigQuery. * * @return {Array.<string>} jobIds The list of all job ids. */ function processReports() { var jobIds = []; // Iterate over each report type. for (var i = 0; i < CONFIG.REPORTS.length; i++) { var reportConfig = CONFIG.REPORTS[i]; Logger.log('Running report %s', reportConfig.NAME); // Get data as csv var csvData = retrieveAdwordsReport(reportConfig); //Logger.log(csvData); // Convert to Blob format. var blobData = Utilities.newBlob(csvData, 'application/octet-stream'); // Load data var jobId = loadDataToBigquery(reportConfig, blobData); jobIds.push(jobId); } return jobIds; } /** * Retrieves AdWords data as csv and formats any fields * to BigQuery expected format. * * @param {Object} reportConfig Report configuration including report name, * conditions, and fields. * * @return {string} csvData Report in csv format. */ function retrieveAdwordsReport(reportConfig) { var fieldNames = Object.keys(reportConfig.FIELDS); var query = 'SELECT ' + fieldNames.join(', ') + ' FROM ' + reportConfig.NAME + '' + reportConfig.CONDITIONS + ' DURING ' + reportConfig.DATE_RANGE; Logger.log(query); var report = AdWordsApp.report(query); var rows = report.rows(); var csvRows = []; // Header row csvRows.push(fieldNames.join(',')); // Iterate over each row. while (rows.hasNext()) { var row = rows.next(); var csvRow = []; for (var i = 0; i < fieldNames.length; i++) { var fieldName = fieldNames[i]; var fieldValue = row[fieldName].toString(); var fieldType = reportConfig.FIELDS[fieldName]; // Strip off % and perform any other formatting here. if (fieldType == 'FLOAT' || fieldType == 'INTEGER') { if (fieldValue.charAt(fieldValue.length - 1) == '%') { fieldValue = fieldValue.substring(0, fieldValue.length - 1); } fieldValue = fieldValue.replace(/,/g,''); if (fieldValue == '--' || fieldValue == 'Unspecified') { fieldValue = '' } } // Add double quotes to any string values. if (fieldType == 'STRING') { if (fieldValue == '--') { fieldValue = '' } fieldValue = fieldValue.replace(/"/g, '""'); fieldValue = '"' + fieldValue + '"' } csvRow.push(fieldValue); } csvRows.push(csvRow.join(',')); } Logger.log('Downloaded ' + reportConfig.NAME + ' with ' + csvRows.length + ' rows.'); return csvRows.join('\n'); } /** * Creates a BigQuery insertJob to load csv data. * * @param {Object} reportConfig Report configuration including report name, * conditions, and fields. * @param {Blob} data Csv report data as an 'application/octet-stream' blob. * * @return {string} jobId The job id for upload. */ function loadDataToBigquery(reportConfig, data) { // Create the data upload job. var job = { configuration: { load: { destinationTable: { projectId: CONFIG.BIGQUERY_PROJECT_ID, datasetId: CONFIG.BIGQUERY_DATASET_ID, tableId: reportConfig.NAME + reportConfig.DATE }, skipLeadingRows: 1 } } }; var insertJob = BigQuery.Jobs.insert(job, CONFIG.BIGQUERY_PROJECT_ID, data); Logger.log('Load job started for %s. Check on the status of it here: ' + 'https://bigquery.cloud.google.com/jobs/%s', reportConfig.NAME, CONFIG.BIGQUERY_PROJECT_ID); return insertJob.jobReference.jobId; } /** * Polls until all jobs are 'DONE'. * * @param {Array.<string>} jobIds The list of all job ids. */ function waitTillJobsComplete(jobIds) { var complete = false; var remainingJobs = jobIds; while (!complete) { if (AdWordsApp.getExecutionInfo().getRemainingTime() < 5){ Logger.log('Script is about to timeout, jobs ' + remainingJobs.join(',') + ' are still incomplete.'); } remainingJobs = getIncompleteJobs(remainingJobs); if (remainingJobs.length == 0) { complete = true; } if (!complete) { Logger.log(remainingJobs.length + ' jobs still being processed.'); // Wait 5 seconds before checking status again. Utilities.sleep(5000); } } Logger.log('All jobs processed.'); } /** * Iterates through jobs and returns the ids for those jobs * that are not 'DONE'. * * @param {Array.<string>} jobIds The list of job ids. * * @return {Array.<string>} remainingJobIds The list of remaining job ids. */ function getIncompleteJobs(jobIds) { var remainingJobIds = []; for (var i = 0; i < jobIds.length; i++) { var jobId = jobIds[i]; var getJob = BigQuery.Jobs.get(CONFIG.BIGQUERY_PROJECT_ID, jobId); if (getJob.status.state != 'DONE') { remainingJobIds.push(jobId); } } return remainingJobIds; } /** * Sends a notification email that jobs have completed loading. * * @param {Array.<string>} jobIds The list of all job ids. */ function sendEmail(jobIds) { var html = []; html.push( '<html>', '<body>', '<table width=800 cellpadding=0 border=0 cellspacing=0>', '<tr>', '<td colspan=2 align=right>', "<div style='font: italic normal 10pt Times New Roman, serif; " + "margin: 0; color: #666; padding-right: 5px;'>" + 'Powered by AdWords Scripts</div>', '</td>', '</tr>', "<tr bgcolor='#3c78d8'>", '<td width=500>', "<div style='font: normal 18pt verdana, sans-serif; " + "padding: 3px 10px; color: white'>Adwords data load to " + "Bigquery report</div>", '</td>', '<td align=right>', "<div style='font: normal 18pt verdana, sans-serif; " + "padding: 3px 10px; color: white'>", AdWordsApp.currentAccount().getCustomerId(), '</tr>', '</table>', '<table width=800 cellpadding=0 border=1 cellspacing=0>', "<tr bgcolor='#ddd'>", "<td style='font: 12pt verdana, sans-serif; " + 'padding: 5px 0px 5px 5px; background-color: #ddd; ' + "text-align: left'>Report</td>", "<td style='font: 12pt verdana, sans-serif; " + 'padding: 5px 0px 5px 5px; background-color: #ddd; ' + "text-align: left'>JobId</td>", "<td style='font: 12pt verdana, sans-serif; " + 'padding: 5px 0px 5x 5px; background-color: #ddd; ' + "text-align: left'>Rows</td>", "<td style='font: 12pt verdana, sans-serif; " + 'padding: 5px 0px 5x 5px; background-color: #ddd; ' + "text-align: left'>State</td>", "<td style='font: 12pt verdana, sans-serif; " + 'padding: 5px 0px 5x 5px; background-color: #ddd; ' + "text-align: left'>ErrorResult</td>", '</tr>', createTableRows(jobIds), '</table>', '</body>', '</html>'); MailApp.sendEmail(CONFIG.RECIPIENT_EMAILS.join(','), 'Adwords data load to Bigquery Complete', '', {htmlBody: html.join('\n')}); } /** * Creates table rows for email report. * * @param {Array.<string>} jobIds The list of all job ids. */ function createTableRows(jobIds) { var html = []; for (var i = 0; i < jobIds.length; i++) { var jobId = jobIds[i]; var job = BigQuery.Jobs.get(CONFIG.BIGQUERY_PROJECT_ID, jobId); var errorResult = '' if (job.status.errorResult) { errorResult = job.status.errorResult; } html.push('<tr>', "<td style='padding: 0px 10px'>" + job.configuration.load.destinationTable.tableId + '</td>', "<td style='padding: 0px 10px'>" + jobId + '</td>', "<td style='padding: 0px 10px'>" + job.statistics.load?job.statistics.load.outputRows:0 + '</td>', "<td style='padding: 0px 10px'>" + job.status.state + '</td>', "<td style='padding: 0px 10px'>" + errorResult + '</td>', '</tr>'); } return html.join('\n'); }
/** * @name Export Data to BigQuery * * @overview The Export Data to BigQuery script sets up a BigQuery * dataset and tables, downloads a report from AdWords and then * loads the report to BigQuery. * * @author AdWords Scripts Team [[email protected]] * * @version 1.3 */ var CONFIG = { BIGQUERY_PROJECT_ID: 'BQ project name', BIGQUERY_DATASET_ID: AdWordsApp.currentAccount().getCustomerId().replace(/-/g, '_'), // Truncate existing data, otherwise will append. TRUNCATE_EXISTING_DATASET: false, TRUNCATE_EXISTING_TABLES: true, // Lists of reports and fields to retrieve from AdWords. REPORTS: [], RECIPIENT_EMAILS: [ 'Your email' ] }; var report = { NAME: 'CLICK_PERFORMANCE_REPORT', //https://developers.google.com/adwords/api/docs/appendix/reports/click-performance-report CONDITIONS: '', FIELDS: {'AccountDescriptiveName': 'STRING', 'AdFormat': 'STRING', 'AdGroupId': 'STRING', 'AdGroupName': 'STRING', 'AoiCountryCriteriaId': 'STRING', 'CampaignId': 'STRING', 'CampaignLocationTargetId': 'STRING', 'CampaignName': 'STRING', 'CampaignStatus': 'STRING', 'Clicks': 'INTEGER', 'ClickType': 'STRING', 'CreativeId': 'STRING', 'CriteriaId': 'STRING', 'CriteriaParameters': 'STRING', 'Date': 'DATE', 'Device': 'STRING', 'ExternalCustomerId': 'STRING', 'GclId': 'STRING', 'KeywordMatchType': 'STRING', 'LopCountryCriteriaId': 'STRING', 'Page': 'INTEGER' }, DATE_RANGE: new Date(new Date().setDate(new Date().getDate()-1)).toISOString().slice(0, 10).replace(/-/g, "")+','+new Date(new Date().setDate(new Date().getDate()-1)).toISOString().slice(0, 10).replace(/-/g, ""), DATE: new Date(new Date().setDate(new Date().getDate()-1)).toISOString().slice(0, 10).replace(/-/g, "") }; //Regular export CONFIG.REPORTS.push(JSON.parse(JSON.stringify(report))); //One-time historical export //for(var i=2;i<91;i++){ // report.DATE_RANGE = new Date(new Date().setDate(new Date().getDate()-i)).toISOString().slice(0, 10).replace(/-/g, "")+','+new Date(new Date().setDate(new Date().getDate()-i)).toISOString().slice(0, 10).replace(/-/g, ""); // report.DATE = new Date(new Date().setDate(new Date().getDate()-i)).toISOString().slice(0, 10).replace(/-/g, ""); // CONFIG.REPORTS.push(JSON.parse(JSON.stringify(report))); //} /** * Main method */ function main() { createDataset(); for (var i = 0; i < CONFIG.REPORTS.length; i++) { var reportConfig = CONFIG.REPORTS[i]; createTable(reportConfig); } var jobIds = processReports(); waitTillJobsComplete(jobIds); sendEmail(jobIds); } /** * Creates a new dataset. * * If a dataset with the same id already exists and the truncate flag * is set, will truncate the old dataset. If the truncate flag is not * set, then will not create a new dataset. */ function createDataset() { if (datasetExists()) { if (CONFIG.TRUNCATE_EXISTING_DATASET) { BigQuery.Datasets.remove(CONFIG.BIGQUERY_PROJECT_ID, CONFIG.BIGQUERY_DATASET_ID, {'deleteContents' : true}); Logger.log('Truncated dataset.'); } else { Logger.log('Dataset %s already exists. Will not recreate.', CONFIG.BIGQUERY_DATASET_ID); return; } } // Create new dataset. var dataSet = BigQuery.newDataset(); dataSet.friendlyName = CONFIG.BIGQUERY_DATASET_ID; dataSet.datasetReference = BigQuery.newDatasetReference(); dataSet.datasetReference.projectId = CONFIG.BIGQUERY_PROJECT_ID; dataSet.datasetReference.datasetId = CONFIG.BIGQUERY_DATASET_ID; dataSet = BigQuery.Datasets.insert(dataSet, CONFIG.BIGQUERY_PROJECT_ID); Logger.log('Created dataset with id %s.', dataSet.id); } /** * Checks if dataset already exists in project. * * @return {boolean} Returns true if dataset already exists. */ function datasetExists() { // Get a list of all datasets in project. var datasets = BigQuery.Datasets.list(CONFIG.BIGQUERY_PROJECT_ID); var datasetExists = false; // Iterate through each dataset and check for an id match. if (datasets.datasets != null) { for (var i = 0; i < datasets.datasets.length; i++) { var dataset = datasets.datasets[i]; if (dataset.datasetReference.datasetId == CONFIG.BIGQUERY_DATASET_ID) { datasetExists = true; break; } } } return datasetExists; } /** * Creates a new table. * * If a table with the same id already exists and the truncate flag * is set, will truncate the old table. If the truncate flag is not * set, then will not create a new table. * * @param {Object} reportConfig Report configuration including report name, * conditions, and fields. */ function createTable(reportConfig) { var tableName = reportConfig.NAME+reportConfig.DATE; if (tableExists(tableName)) { if (CONFIG.TRUNCATE_EXISTING_TABLES) { BigQuery.Tables.remove(CONFIG.BIGQUERY_PROJECT_ID, CONFIG.BIGQUERY_DATASET_ID, tableName); Logger.log('Truncated table %s.', tableName); } else { Logger.log('Table %s already exists. Will not recreate.', tableName); return; } } // Create new table. var table = BigQuery.newTable(); var schema = BigQuery.newTableSchema(); var bigQueryFields = []; // Add each field to table schema. var fieldNames = Object.keys(reportConfig.FIELDS); for (var i = 0; i < fieldNames.length; i++) { var fieldName = fieldNames[i]; var bigQueryFieldSchema = BigQuery.newTableFieldSchema(); bigQueryFieldSchema.description = fieldName; bigQueryFieldSchema.name = fieldName; bigQueryFieldSchema.type = reportConfig.FIELDS[fieldName]; bigQueryFields.push(bigQueryFieldSchema); } schema.fields = bigQueryFields; table.schema = schema; table.friendlyName = tableName; table.tableReference = BigQuery.newTableReference(); table.tableReference.datasetId = CONFIG.BIGQUERY_DATASET_ID; table.tableReference.projectId = CONFIG.BIGQUERY_PROJECT_ID; table.tableReference.tableId = tableName; table = BigQuery.Tables.insert(table, CONFIG.BIGQUERY_PROJECT_ID, CONFIG.BIGQUERY_DATASET_ID); Logger.log('Created table with id %s.', table.id); } /** * Checks if table already exists in dataset. * * @param {string} tableId The table id to check existence. * * @return {boolean} Returns true if table already exists. */ function tableExists(tableId) { // Get a list of all tables in the dataset. var tables = BigQuery.Tables.list(CONFIG.BIGQUERY_PROJECT_ID, CONFIG.BIGQUERY_DATASET_ID); var tableExists = false; // Iterate through each table and check for an id match. if (tables.tables != null) { for (var i = 0; i < tables.tables.length; i++) { var table = tables.tables[i]; if (table.tableReference.tableId == tableId) { tableExists = true; break; } } } return tableExists; } /** * Process all configured reports * * Iterates through each report to: retrieve AdWords data, * backup data to Drive (if configured), load data to BigQuery. * * @return {Array.<string>} jobIds The list of all job ids. */ function processReports() { var jobIds = []; // Iterate over each report type. for (var i = 0; i < CONFIG.REPORTS.length; i++) { var reportConfig = CONFIG.REPORTS[i]; Logger.log('Running report %s', reportConfig.NAME); // Get data as csv var csvData = retrieveAdwordsReport(reportConfig); //Logger.log(csvData); // Convert to Blob format. var blobData = Utilities.newBlob(csvData, 'application/octet-stream'); // Load data var jobId = loadDataToBigquery(reportConfig, blobData); jobIds.push(jobId); } return jobIds; } /** * Retrieves AdWords data as csv and formats any fields * to BigQuery expected format. * * @param {Object} reportConfig Report configuration including report name, * conditions, and fields. * * @return {string} csvData Report in csv format. */ function retrieveAdwordsReport(reportConfig) { var fieldNames = Object.keys(reportConfig.FIELDS); var query = 'SELECT ' + fieldNames.join(', ') + ' FROM ' + reportConfig.NAME + '' + reportConfig.CONDITIONS + ' DURING ' + reportConfig.DATE_RANGE; Logger.log(query); var report = AdWordsApp.report(query); var rows = report.rows(); var csvRows = []; // Header row csvRows.push(fieldNames.join(',')); // Iterate over each row. while (rows.hasNext()) { var row = rows.next(); var csvRow = []; for (var i = 0; i < fieldNames.length; i++) { var fieldName = fieldNames[i]; var fieldValue = row[fieldName].toString(); var fieldType = reportConfig.FIELDS[fieldName]; // Strip off % and perform any other formatting here. if (fieldType == 'FLOAT' || fieldType == 'INTEGER') { if (fieldValue.charAt(fieldValue.length - 1) == '%') { fieldValue = fieldValue.substring(0, fieldValue.length - 1); } fieldValue = fieldValue.replace(/,/g,''); if (fieldValue == '--' || fieldValue == 'Unspecified') { fieldValue = '' } } // Add double quotes to any string values. if (fieldType == 'STRING') { if (fieldValue == '--') { fieldValue = '' } fieldValue = fieldValue.replace(/"/g, '""'); fieldValue = '"' + fieldValue + '"' } csvRow.push(fieldValue); } csvRows.push(csvRow.join(',')); } Logger.log('Downloaded ' + reportConfig.NAME + ' with ' + csvRows.length + ' rows.'); return csvRows.join('\n'); } /** * Creates a BigQuery insertJob to load csv data. * * @param {Object} reportConfig Report configuration including report name, * conditions, and fields. * @param {Blob} data Csv report data as an 'application/octet-stream' blob. * * @return {string} jobId The job id for upload. */ function loadDataToBigquery(reportConfig, data) { // Create the data upload job. var job = { configuration: { load: { destinationTable: { projectId: CONFIG.BIGQUERY_PROJECT_ID, datasetId: CONFIG.BIGQUERY_DATASET_ID, tableId: reportConfig.NAME + reportConfig.DATE }, skipLeadingRows: 1 } } }; var insertJob = BigQuery.Jobs.insert(job, CONFIG.BIGQUERY_PROJECT_ID, data); Logger.log('Load job started for %s. Check on the status of it here: ' + 'https://bigquery.cloud.google.com/jobs/%s', reportConfig.NAME, CONFIG.BIGQUERY_PROJECT_ID); return insertJob.jobReference.jobId; } /** * Polls until all jobs are 'DONE'. * * @param {Array.<string>} jobIds The list of all job ids. */ function waitTillJobsComplete(jobIds) { var complete = false; var remainingJobs = jobIds; while (!complete) { if (AdWordsApp.getExecutionInfo().getRemainingTime() < 5){ Logger.log('Script is about to timeout, jobs ' + remainingJobs.join(',') + ' are still incomplete.'); } remainingJobs = getIncompleteJobs(remainingJobs); if (remainingJobs.length == 0) { complete = true; } if (!complete) { Logger.log(remainingJobs.length + ' jobs still being processed.'); // Wait 5 seconds before checking status again. Utilities.sleep(5000); } } Logger.log('All jobs processed.'); } /** * Iterates through jobs and returns the ids for those jobs * that are not 'DONE'. * * @param {Array.<string>} jobIds The list of job ids. * * @return {Array.<string>} remainingJobIds The list of remaining job ids. */ function getIncompleteJobs(jobIds) { var remainingJobIds = []; for (var i = 0; i < jobIds.length; i++) { var jobId = jobIds[i]; var getJob = BigQuery.Jobs.get(CONFIG.BIGQUERY_PROJECT_ID, jobId); if (getJob.status.state != 'DONE') { remainingJobIds.push(jobId); } } return remainingJobIds; } /** * Sends a notification email that jobs have completed loading. * * @param {Array.<string>} jobIds The list of all job ids. */ function sendEmail(jobIds) { var html = []; html.push( '<html>', '<body>', '<table width=800 cellpadding=0 border=0 cellspacing=0>', '<tr>', '<td colspan=2 align=right>', "<div style='font: italic normal 10pt Times New Roman, serif; " + "margin: 0; color: #666; padding-right: 5px;'>" + 'Powered by AdWords Scripts</div>', '</td>', '</tr>', "<tr bgcolor='#3c78d8'>", '<td width=500>', "<div style='font: normal 18pt verdana, sans-serif; " + "padding: 3px 10px; color: white'>Adwords data load to " + "Bigquery report</div>", '</td>', '<td align=right>', "<div style='font: normal 18pt verdana, sans-serif; " + "padding: 3px 10px; color: white'>", AdWordsApp.currentAccount().getCustomerId(), '</tr>', '</table>', '<table width=800 cellpadding=0 border=1 cellspacing=0>', "<tr bgcolor='#ddd'>", "<td style='font: 12pt verdana, sans-serif; " + 'padding: 5px 0px 5px 5px; background-color: #ddd; ' + "text-align: left'>Report</td>", "<td style='font: 12pt verdana, sans-serif; " + 'padding: 5px 0px 5px 5px; background-color: #ddd; ' + "text-align: left'>JobId</td>", "<td style='font: 12pt verdana, sans-serif; " + 'padding: 5px 0px 5x 5px; background-color: #ddd; ' + "text-align: left'>Rows</td>", "<td style='font: 12pt verdana, sans-serif; " + 'padding: 5px 0px 5x 5px; background-color: #ddd; ' + "text-align: left'>State</td>", "<td style='font: 12pt verdana, sans-serif; " + 'padding: 5px 0px 5x 5px; background-color: #ddd; ' + "text-align: left'>ErrorResult</td>", '</tr>', createTableRows(jobIds), '</table>', '</body>', '</html>'); MailApp.sendEmail(CONFIG.RECIPIENT_EMAILS.join(','), 'Adwords data load to Bigquery Complete', '', {htmlBody: html.join('\n')}); } /** * Creates table rows for email report. * * @param {Array.<string>} jobIds The list of all job ids. */ function createTableRows(jobIds) { var html = []; for (var i = 0; i < jobIds.length; i++) { var jobId = jobIds[i]; var job = BigQuery.Jobs.get(CONFIG.BIGQUERY_PROJECT_ID, jobId); var errorResult = '' if (job.status.errorResult) { errorResult = job.status.errorResult; } html.push('<tr>', "<td style='padding: 0px 10px'>" + job.configuration.load.destinationTable.tableId + '</td>', "<td style='padding: 0px 10px'>" + jobId + '</td>', "<td style='padding: 0px 10px'>" + job.statistics.load?job.statistics.load.outputRows:0 + '</td>', "<td style='padding: 0px 10px'>" + job.status.state + '</td>', "<td style='padding: 0px 10px'>" + errorResult + '</td>', '</tr>'); } return html.join('\n'); }

قبل تشغيل البرنامج النصي ، تأكد من النقر فوق الزر معاينة في الزاوية اليمنى السفلية للتحقق من النتيجة. في حالة وجود أخطاء فيه ، فسيقوم النظام بتنبيهك والإشارة إلى السطر الذي حدث فيه ، كما في لقطة الشاشة هذه:

قم بتشغيل البرنامج النصي

إذا لم تكن هناك أخطاء ، فانقر فوق الزر "تشغيل":

إعداد التحميل من إعلانات Google

نتيجة لذلك ، ستتلقى تقرير CLICK_PERFORMANCE_REPORT جديدًا في GBQ الخاص بك والذي سيكون متاحًا في اليوم التالي:

النتائج في GBQ

تذكر أنه عند استخدام نقل البيانات ، فإنك تحصل على كمية كبيرة من البيانات الأولية غير المجمعة. باستخدام Ads Script ، سيكون لديك معلومات حول حقول معينة فقط.

الحقول التالية من هذا التحميل مضمنة في جداول OWOX BI المتعلقة بالجلسة:

  • GclId
  • معرف الحملة
  • اسم الحملة
  • AdGroupId
  • اسم المجموعة الإعلانية
  • المعايير
  • المعايير
  • KeywordMatchType

كيفية توصيل تنزيل البيانات من إعلانات Google بـ OWOX BI

أنت الآن بحاجة إلى دمج المعلومات من إعلانات Google مع بيانات الموقع لفهم الحملات التي وصل المستخدمون إلى موقعك من خلالها. لا تحتوي الجداول التي تحصل عليها في BigQuery ، مثل نقل البيانات ، على معلمة معرّف العميل. يمكنك فقط تحديد العميل الذي نقر على الإعلانات عن طريق ربط بيانات gclid ببيانات تدفق OWOX BI.

إذا لم يكن لديك خط تدفق Google Analytics → Google BigQuery في OWOX BI حتى الآن ، فاقرأ الإرشادات حول كيفية إنشائه.

ثم انتقل إلى مشروع OWOX BI وافتح خط الأنابيب هذا. انقر فوق علامة التبويب الإعدادات ، وضمن جمع بيانات الجلسة ، انقر فوق تحرير الإعدادات:

إعدادات خط أنابيب OWOX BI

استخدم شريط التمرير لتمكين جمع البيانات للحملات ذات العلامات التلقائية في إعلانات Google ، وانقر على تغيير الإعدادات:

تمكين جمع البيانات لبرنامج إعلانات Google

حدد نوع الترميز AutoLabel ، وحدد كيفية تحميل نقل البيانات أو نصوص الإعلانات إلى BigQuery. حدد المشروع ومجموعة البيانات التي سيتم تنزيل بيانات إعلانات Google منها واحفظ إعداداتك:

احفظ الإعدادات

نصائح مفيدة

نصيحة 1. باستخدام نقل البيانات ، يمكنك تحميل البيانات التاريخية من إعلانات Google إلى GBQ. في الوقت نفسه ، لا توجد قيود على إجمالي فترة التحميل (إما لمدة عام أو لمدة ثلاثة) ، ولكن مع البيانات لمدة 180 يومًا فقط في كل مرة.

يمكنك تنشيط التحميل وتحديد الفترة الزمنية باستخدام زر جدولة إعادة التعبئة في علامة التبويب التحويلات عن طريق تحديد التحويل الذي تريده:

تحميل البيانات التاريخية

النصيحة 2. إذا كنت ترغب في التحقق من عدد حسابات إعلانات Google التي سيخصم برنامج "شركاء Google المعتمدون" عليها ، فسيلزمك تحديد عدد "معرّف العميل الخارجي" في جدول "العميل" باستخدام الاستعلام:

    SELECT ExternalCustomerId FROM `project_name.dataset_name.Customer_*` WHERE _PARTITIONTIME >= "2020-01-01 00:00:00" AND _PARTITIONTIME < "2020-07-10 00:00:00" group by 1
SELECT ExternalCustomerId FROM `project_name.dataset_name.Customer_*` WHERE _PARTITIONTIME >= "2020-01-01 00:00:00" AND _PARTITIONTIME < "2020-07-10 00:00:00" group by 1

يمكنك تحرير التواريخ في الاستعلام.

نصيحة 3. يمكنك الوصول إلى البيانات التي تم تحميلها بنفسك باستخدام استعلامات SQL. هنا ، على سبيل المثال ، هو استعلام لتحديد فعالية الحملات من جداول "الحملة" و "CampaignBasicStats" المشتقة من نقل البيانات:

    SELECT {source language="sql"} c.ExternalCustomerId, c.CampaignName, c.CampaignStatus, SUM(cs.Impressions) AS Impressions, SUM(cs.Interactions) AS Interactions, {/source} (SUM(cs.Cost) / 1000000) AS Cost FROM `[DATASET].Campaign_[CUSTOMER_ID]` c LEFT JOIN {source language="sql"} {source language="sql"} `[DATASET].CampaignBasicStats_[CUSTOMER_ID]` cs ON (c.CampaignId = cs.CampaignId AND cs._DATA_DATE BETWEEN DATE_ADD(CURRENT_DATE(), INTERVAL -31 DAY) AND DATE_ADD(CURRENT_DATE(), INTERVAL -1 DAY)) WHERE c._DATA_DATE = c._LATEST_DATE GROUP BY 1, 2, 3 ORDER BY Impressions DESC
SELECT {source language="sql"} c.ExternalCustomerId, c.CampaignName, c.CampaignStatus, SUM(cs.Impressions) AS Impressions, SUM(cs.Interactions) AS Interactions, {/source} (SUM(cs.Cost) / 1000000) AS Cost FROM `[DATASET].Campaign_[CUSTOMER_ID]` c LEFT JOIN {source language="sql"} {source language="sql"} `[DATASET].CampaignBasicStats_[CUSTOMER_ID]` cs ON (c.CampaignId = cs.CampaignId AND cs._DATA_DATE BETWEEN DATE_ADD(CURRENT_DATE(), INTERVAL -31 DAY) AND DATE_ADD(CURRENT_DATE(), INTERVAL -1 DAY)) WHERE c._DATA_DATE = c._LATEST_DATE GROUP BY 1, 2, 3 ORDER BY Impressions DESC

ملاحظة: إذا كنت بحاجة إلى مساعدة في تحميل ودمج البيانات في Google BigQuery ، فنحن على استعداد للمساعدة. اشترك للحصول على عرض توضيحي - وسنناقش التفاصيل.

سجل للحصول على نسخة تجريبية