الدمج مع B&A بصفتك بائعًا

خدمات عروض الأسعار والمزادات (B&A) هي مجموعة من الخدمات للمشترين والبائعين للإعلانات، وتعمل في بيئة تنفيذ موثوقة (TEE) لتسهيل إجراء مزاد Protected Audience (PA). يوضّح دليل المطوّرين هذا كيف يمكن للبائع الدمج مع مزاد "الميزة المحسّنة للخصوصية" في Chrome من أجل B&A.

جولة تفصيلية

مسار دمج البائع حيث يحصل رمز JavaScript على حمولة مزاد "العطاءات والشراء" التي يتم إرسالها إلى SAS، وترسل SAS الطلب إلى واجهة البائع الأمامية (SFE). تعرض خدمة SFE النتيجة التي يجب أن تحوّلها خدمة SAS إلى المتصفّح، وتنفّذ رموز JavaScript الخاصة بالبائع طلبات runAdAuction

يمكن تلخيص الخطوات على النحو التالي:

  1. الاتصال بالرقم getInterestGroupAdAuctionData() للحصول على الحمولة المشفّرة من المتصفّح
  2. الاتصال بـ fetch('https://your-ad-server.example') وإرسال طلب المزاد الموحّد مع الحمولة المشفّرة إلى نظام SAS
  3. الاتصال بعملية SelectAd() الخاصة بفريق المبيعات الميداني من نظام SAS لتنفيذ مزاد B&A
  4. إرجاع نتيجة المزاد B&A إلى الصفحة مع قيمة التجزئة للردّ
  5. استدعاء runAdAuction() في المتصفّح لتنفيذ مزاد "الخصوصية الآمنة" لبائع واحد أو وضع مختلط أو بائعين متعدّدين، وتمرير نتيجة مزاد "عروض الأسعار والمزادات" من جهة الخادم إلى عملية الاستدعاء

الحصول على بيانات مشفّرة عن مزاد الإعلانات

مخطّط انسيابي نفسه مع تمييز الخطوة الأولى، وهي عندما يستدعي رمز JavaScript الخاص بالبائع الدالة getInterestGroupAdAuctionData

للحصول على البيانات اللازمة لتنفيذ مزاد "العرض الإعلاني بدون انقطاع" و"المزايدة" من جهة الخادم، يستدعي رمز JavaScript الخاص بالبائع على صفحة الناشر navigator.getInterestGroupAdAuctionData().

const adAuctionData = await navigator.getInterestGroupAdAuctionData({
  seller: 'https://ssp.example', // Required
  requestSize: 51200,
  coordinatorOrigin: 'https://publickeyservice.pa.gcp.privacysandboxservices.com/',
  perBuyerConfig: {
    'https://dsp-x.example': { targetSize: 8192 },
    'https://dsp-y.example': { targetSize: 8192 }
  }
});

const { requestId, request } = adAuctionData;
الحقل الوصف
seller مَعلمة مطلوبة. تمثّل هذه السمة بلد البائع الذي يدير المزاد. يجب أن تتطابق هذه القيمة مع قيمة seller في طلب runAdAuction() لاحقًا.
requestSize Optional. تضبط هذه السمة الحدّ الأقصى لحجم الحمولة لجميع بيانات المشترين. راجِع قسم حجم الطلب في الفيديو التوضيحي لمعرفة المزيد.
perBuyerConfig Optional. تضبط هذه السمة إعدادات كل مشترٍ، كما تتحكّم في المشترين الذين يشاركون في مزاد "العرض الأول" و"المزايدة".

إذا كانت مصادر المشترين مُدرَجة في perBuyerConfig، سيتم تضمين بيانات مجموعات اهتمامات المشترين هذه فقط في الحمولة. إذا لم يتم إدراج أي مشترين في perBuyerConfig، سيتم تضمين جميع فئات الاهتمامات الخاصة بالمستخدم في الحمولة.

targetSize اختيارية في حال ضبط requestSize. مطلوبة إذا تم ضبط مصدر المشتري في perBuyerConfig ولكن لم يتم ضبط requestSize.

تضبط هذه السمة الحد الأقصى لحجم الحمولة لبيانات المشتري. راجِع قسم حجم الطلب في الفيديو التوضيحي لمعرفة المزيد.

coordinatorOrigin اختياري، ولكن سيصبح مطلوبًا في النهاية. يتم ضبط القيمة التلقائية على https://publickeyservice.pa.gcp.privacysandboxservices.com في حال عدم ضبطها.

تضبط هذه السمة المنسّق المستخدَم لجلب المفتاح اللازم لتشفير الحمولة. راجِع قسم المنسّق في الشرح لمعرفة المزيد.

عند إجراء الطلب، يقرأ المتصفّح مجموعات اهتمامات المشترين المُدرَجين في perBuyerConfig، ويشفّر بيانات المشترين. تحتوي بيانات المشتري هذه على معلومات من مواقع إلكترونية متعددة سيتم استخدامها لتقديم عروض الأسعار، ولا يمكن فك تشفيرها خارج بيئة تنفيذ موثوقة (TEE). لتحسين الحمولة، يتم تضمين اسم مجموعة الاهتمامات ومفاتيح إشارات عروض الأسعار الموثوقة وإشارات المتصفّح فقط في الحمولة.

في عنصر بيانات مزاد الإعلانات الذي تعرضه استدعاء getInterestGroupAdAuctionData()، تتوفّر السلسلة requestId ومصفوفة البايت المشفرة request.

لقطة شاشة من "أدوات مطوّري البرامج في Chrome" تعرض الطلب ومعرّف الطلب المتاحَين في بيانات مزاد الإعلانات

يتم استخدام السلسلة requestId لاحقًا عند استدعاء runAdAuction() لإنهاء المزاد في المتصفّح. يتم إرسال حمولة request المشفرة إلى "خدمة إعلانات البائع" كجزء من طلب المزاد الموحّد.

للاطّلاع على مثال على هذا الطلب، يمكنك الاطّلاع على رمز JavaScript الخاص بالبائع في تطبيق الاختبار المحلي.

إرسال طلب المزاد الموحّد إلى SAS

مخطّط بياني مماثل يوضّح الخطوة الثانية، وهي عندما يرسل رمز JavaScript الخاص بالبائع طلب مزاد موحّد إلى SAS

طلب المزاد الموحّد هو طلب يتضمّن حمولة المزاد السياقي بنص عادي وحمولة مزاد "الموافقة على الإعلانات المخصّصة" و"الموافقة على التحليلات". حمولة مزاد PA B&A هي بيانات request المشفّرة التي أنشأها المتصفّح في طلب getInterestGroupAdAuctionData(). يتم إرسال هذا الطلب إلى SAS، حيث يتم تنسيق المزاد المستند إلى السياق ومزاد "الميزة الإعلانية المخصّصة" و"اتفاقية المعالجة".

fetch('https://ssp.example/ad-auction', {
  method: 'POST',
  adAuctionHeaders: true,
  body: JSON.stringify({
    contextualAuctionPayload: { somePayload },
    protectedAudienceAuctionPayload: encodeBinaryData(request)
  }),
});

لإرسال الطلب إلى SAS، يتم إجراء مكالمة fetch() من الصفحة:

  • يجب أن يتضمّن الطلب الخيار adAuctionHeaders: true، الذي يطلب من المتصفّح التحقّق من استجابة هذا الطلب في وقت لاحق عند استدعاء runAdAuction() لإنهاء المزاد في المتصفّح.
  • يجب أن يتطابق مصدر طلب الجلب مع مصدر seller المقدَّم إلى طلبات getInterestGroupAdAuctionData() وrunAdAuction().

يتضمّن نص المكالمة ما يلي:

  1. حمولة المزاد السياقي بنص عادي التي ستستخدمها SAS لتنفيذ المزاد السياقي.
  2. حمولة مزاد Protected Audience المشفّرة التي سترسلها SAS إلى SFE لإجراء مزاد B&A من جهة الخادم

للاطّلاع على مثال على هذا الطلب، يمكنك الاطّلاع على رمز JavaScript الخاص بالبائع في تطبيق الاختبار المحلي.

الترميز وفك الترميز باستخدام Base64

إنّ حمولة request المشفّرة التي يتم إرجاعها من طلب getInterestGroupAdAuctionData() هي مثيل من Uint8Array، وهو نوع بيانات لا يمكن لـ JSON التعامل معه. لإرسال مصفوفة البايت بتنسيق JSON، يمكنك تطبيق ترميز base64 على البيانات الثنائية لتحويلها إلى سلسلة.

توفّر واجهة برمجة التطبيقات المستندة إلى متصفّح JavaScript الدالتَين atob() وbtoa() على window اللتَين تحوّلان بين البيانات الثنائية وسلسلة ASCII المرمّزة باستخدام Base64. (atob تعني تحويل ASCII إلى ثنائي، وbtoa تعني تحويل الثنائي إلى ASCII).

يبدو استدعاء btoa() لترميز البيانات الثنائية إلى سلسلة مرمّزة بـ base64 على النحو التالي:

function encodeBinaryData(data) {
  return btoa(String.fromCharCode.apply(null, data));
}

إنّ نتيجة مزاد B&A المشفّرة التي يتم عرضها من خلال طلب fetch هذا تكون أيضًا بترميز base64، لذا عليك فك ترميزها مرة أخرى إلى بيانات ثنائية. استدعِ الدالة atob() لفك ترميز سلسلة ASCII المرمّزة باستخدام Base64 إلى بيانات ثنائية:

function decodeBase64String(base64string) {
  return new Uint8Array(
    atob(base64string)
      .split('')
      .map((char) => char.charCodeAt(0))
  );
}

ومع ذلك، تكون السلسلة بترميز base64 أكبر عادةً بنسبة% 33 تقريبًا من البيانات الأصلية. إذا أردت تحسين وقت الاستجابة بشكل أكبر، استخدِم تنسيقًا آخر غير JSON لإرسال البيانات الثنائية.

اتّصِل بفريق مبيعات Google SelectAd لتنفيذ مزاد "العطاءات والميزانية"

مخطّط توضيحي مماثل مع تمييز الخطوة الثالثة، وهي عندما يرسل SAS طلب SelectAd إلى SFE، ويُجري SFE مزادًا على "العطاءات والشراء"

بعد أن تتلقّى "خدمة إعلانات البائع" طلب المزاد الموحّد من الصفحة، يتمّ إجراء المزاد المستند إلى السياق أولاً لتحديد الفائز في المزاد المستند إلى السياق وجمع إشارات المعلِن التي سيتمّ تمريرها إلى مزاد "الإعلانات المخصّصة حسب الاهتمامات" و"الجمهور المشابه". بعد ذلك، يتم بدء مزاد B&A من خلال استدعاء عملية SelectAd في SFE من SAS مع حمولة الطلب. يُرجى العِلم أنّه يتم إعادة توجيه بعض البيانات الوصفية من طلب الصفحة إلى SAS في الخطوة 2 إلى SFE.

إنشاء حمولة SelectAdRequest

يمكن إنشاء حمولة طلب الاتصال SelectAd على النحو التالي:

const selectAdRequest = {
  auction_config: {
    seller: 'https://ssp.example',
    auction_signals: '{"testKey":"someValue"}',
    seller_signals: '{"testKey":"someValue"}',
    buyer_list: [
      'https://dsp-x.example',
      'https://dsp-y.example',
    ],
    per_buyer_config: {
      'https://dsp-x.example': { buyer_signals: '{"testKey": "someValue"}' },
      'https://dsp-y.example': { buyer_signals: '{"testKey": "someValue"}' },
    },
  },
  client_type: 'CLIENT_TYPE_BROWSER',
  protected_auction_ciphertext: decodeBase64string(request)
};

يُرجى العِلم أنّه إذا تم ترميز بيانات مزاد الإعلانات المشفّرة من المتصفّح باستخدام base64، يجب إعادة فك ترميزها إلى بيانات ثنائية إذا تم إرسال الطلب إلى SFE باستخدام gRPC. إذا تم إرسال الطلب باستخدام HTTP، يمكن أن تبقى بيانات مزاد الإعلانات المشفّرة في شكلها المرمّز بترميز base64.

للاطّلاع على الحقول الأخرى المحدّدة في طلب SelectAd، يُرجى الاطّلاع على تعريف proto الخاص بـ SelectAdRequest.

ضبط حقل البائع على المستوى الأعلى للمزادات المختلطة ومزادات المكوّنات

إذا كان البائع يدير مزادًا مختلط الوضع أو يشارك كبائع مكوّن في مزاد متعدّد البائعين، يجب تحديد الحقل top_level_seller في الطلب.

إذا كنت بائعًا يستخدم وضعًا مختلطًا، تكون القيمة top_level_seller هي بلد المنشأ:

const selectAdRequest = {
  auction_config: {
    seller: 'https://ssp-mix.example',
    top_level_seller: 'https://ssp-mix.example',
  }
}

إذا كنت بائعًا لمكوّنات، تكون قيمة top_level_seller هي البائع الأعلى مستوى في مزاد البائعين المتعدّدين:

const selectAdRequest = {
  auction_config: {
    seller: 'https://ssp-mix.example',
    top_level_seller: 'https://ssp-top.example',
  }
}

الاتصال بالرقم SelectAd الخاص بـ SFE

يمكن إجراء المكالمة إلى SFE من SAS باستخدام gRPC أو HTTP.

مكالمة gRPC

يبدو طلب gRPC إلى SFE على النحو التالي باستخدام Express في Node مع عميل gRPC:

import grpc from '@grpc/grpc-js';

// Load proto definition
const packageDefinition = protoLoader.loadSync(protoPath, { keepCase: true, enums: String });

const {
  privacy_sandbox: {
    bidding_auction_servers: { SellerFrontEnd }
  }
} = grpc.loadPackageDefinition(packageDefinition);

// Instantiate the gRPC client
const sfeGrpcClient = new SellerFrontEnd('192.168.84.104:50067', grpc.credentials.createInsecure());

// Send SelectAd request
sfeGrpcClient.selectAd(selectAdRequest,(error, response) => {
  // Handle SFE response
});

يمكن العثور على تعريف البروتوكول لبرنامج SFE في مستودع تطبيق الاختبار المحلي.

طلب HTTP إلى خادم Envoy الوكيل

يتم إرسال طلب HTTP POST إلى SFE إلى المسار /v1/selectAd، ويكون على النحو التالي:

fetch('https://ssp-ba.example/sfe/v1/selectAd', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(selectAdRequest),
});

إعادة توجيه البيانات الوصفية

يجب إضافة البيانات الوصفية التالية من طلب الصفحة إلى SAS إلى طلب SelectAd من SAS إلى SFE:

عند إرسال البيانات الوصفية إلى SFE، يجب استخدام العناوين غير العادية التالية لأنّ gRPC قد يغيّر العنوان User-Agent:

  • X-Accept-Language
  • X-User-Agent
  • X-BnA-Client-IP

في ما يلي مثال على كيفية إعادة توجيه البيانات الوصفية باستخدام Express في Node مع عميل gRPC:

sellerAdService.post('/ad-auction', (req, res) => {
  // …
  const metadata = new grpc.Metadata();
  metadata.add('X-Accept-Language', req.header('Accept-Language'));
  metadata.add('X-User-Agent', req.header('User-Agent'));
  metadata.add('X-BnA-Client-IP', req.ip);

  const sfeGrpcClient = createSfeGrpcClient();
  sfeGrpcClient.selectAd(selectAdRequest, metadata, callbackFn);
})

في ما يلي مثال على كيفية إعادة توجيه البيانات الوصفية باستخدام طلب HTTP:

sellerAdService.post('/ad-auction', (req, res) => {
  // …
  fetch('https://ssp-ba.example/sfe/v1/selectAd', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Accept-Language': req.header('Accept-Language'),
      'X-User-Agent': req.header('User-Agent'),
      'X-BnA-Client-IP': req.ip
    },
    body: JSON.stringify(selectAdRequest)
  });
})

المزاد المتعدّد البائعين الذي تتم إدارته من جهة الخادم

إذا كنت بائعًا على أعلى مستوى وتدير مزادًا متعدد البائعين يتم تنظيمه من الخادم، يتم إجراء طلب GetComponentAuctionCiphertexts إلى SFE قبل إجراء طلب SelectAd. تحتوي الاستجابة على حمولات مزاد المكوّنات التي تمّت إعادة تشفيرها والتي يتمّ إرسالها إلى خدمات إعلانات البائعين. يتم تقديم نتائج مزاد الإعلانات على مستوى المكوّن B&A الذي تم عرضه إلى طلب SelectAd الخاص بميزة SFE للبائع على أعلى مستوى.

يمكنك الاطّلاع على شرح الحسابات المتعددة البائعين على GitHub لمعرفة المزيد.

إرجاع نتيجة مزاد "العطاءات الآلية" و"العطاءات اليدوية" إلى الصفحة

مخطّط توضيحي مماثل مع تمييز الخطوة الرابعة، وهي عندما يرسل SAS نتيجة مزاد SelectAd إلى المتصفّح

بعد انتهاء مزاد "العرض والمزايدة"، يتم إرجاع نتيجة المزاد المشفّرة إلى SAS، وتردّ SAS على طلب المزاد الموحّد من الصفحة في الخطوة 2 بنتيجة المزاد المشفّرة. في استجابة SAS للصفحة، يتم ضبط قيمة تجزئة SHA-256 المرمّزة باستخدام base64url لنتيجة المزاد المشفّرة في عنوان الاستجابة Ad-Auction-Result. يستخدم المتصفّح هذه التجزئة للتحقّق من الحمولة عند إنهاء المزاد في العميل.

يبدو إنشاء تجزئة SHA-256 باستخدام ترميز base64 على النحو التالي في Node:

import { createHash } from 'crypto';

createHash('sha256')
  .update(binaryData, 'base64')
  .digest('base64url');

يبدو ربط التجزئة في عنوان الاستجابة وعرض نتيجة المزاد على الصفحة كما يلي:

sellerAdService.post('/ad-auction', (req, res) => {
  // …
  sfeGrpcClient.selectAd(selectAdRequest, metadata, (error, response) => {
    const { auction_result_ciphertext } = response;

    const ciphertextShaHash = createHash('sha256')
      .update(auction_result_ciphertext, 'base64')
      .digest('base64url');

    res.set('Ad-Auction-Result', ciphertextShaHash);

    res.json({
      protectedAudienceAuctionResult: encodeBinaryData(auction_result_ciphertext),
      contextualAuctionResult: getContextualAuctionResult()
    });
  });
})

بما أنّ هذه استجابة لطلب المزاد الموحّد الذي تم إجراؤه من الصفحة في الخطوة 2، يتم أيضًا تضمين نتيجة المزاد المستند إلى السياق في الاستجابة.

يمكن تضمين تجزئات متعددة في Ad-Auction-Result من خلال تكرار العنوان أو فصل التجزئات. عنوانا الاستجابة التاليان متكافئان:

Ad-Auction-Result: ungWv48Bz-pBQUDeXa4iI7ADYaOWF3qctBD_YfIAFa0=,9UTB-u-WshX66Xqz5DNCpEK9z-x5oCS5SXvgyeoRB1k=
Ad-Auction-Result: ungWv48Bz-pBQUDeXa4iI7ADYaOWF3qctBD_YfIAFa0=
Ad-Auction-Result: 9UTB-u-WshX66Xqz5DNCpEK9z-x5oCS5SXvgyeoRB1k=

للاطّلاع على مثال على هذا الطلب، راجِع رمز خادم البائع لتطبيق الاختبار المحلي.

الاتصال بالرقم runAdAuction() لإكمال المزاد

مخطّط انسيابي نفسه مع تمييز الخطوة الخامسة، وهي عندما ينفّذ رمز JavaScript من جهة العميل المزاد ويقدّم استجابة الخادم.

يتضمّن الردّ الموحّد للمزاد الذي يتم إرجاعه من SAS نتيجة المزاد المشفّرة الخاصة بالعرض والسعر. يتم تمرير الحمولة إلى طلب runAdAuction() لإنهاء المزاد في المتصفّح. يتم أيضًا تمرير قيمة requestId من طلب getInterestGroupAdAuctionData() في الخطوة 1 إلى المزاد.

// Get the encrypted ad auction data (Step #1)
const { requestId, request } = navigator.getInterestGroupAdAuctionData(adAuctionDataConfig)

// Send unified auction request (Step #2)
const response = await fetch('https://ssp-ba.example/ad-auction', {
  method: 'POST',
  body: JSON.stringify({
    adAuctionRequest: encodeBinaryData(request),
  }),
});

const { protectedAudienceAuctionResult } = await response.json();

// Finish the auction in the browser
await navigator.runAdAuction({
  // pass in "requestId" and "protectedAudienceAuctionResult"
  // the config structure will differ based on the auction configuration
});

يختلف بنية إعدادات المزاد التي يتم تمريرها إلى طلب runAdAuction() استنادًا إلى إعدادات المزاد التي يختارها البائع.

مزاد البائع الواحد

لتنفيذ مزاد B&A لبائع واحد، يتم إنشاء إعدادات المزاد الخاصة بطلب runAdAuction() على النحو التالي:

await navigator.runAdAuction({
  seller: 'https://ssp-ba.example',
  requestId,
  serverResponse: protectedAudienceAuctionResult,
});

يقبل الحقل requestId القيمة requestId التي تعرضها استدعاء الدالة getInterestGroupAdAuctionData(). يقبل الحقل serverResponse مصفوفة بايت لمزاد B&A الذي تم تنفيذه في الخطوة رقم 3.

للاطّلاع على مثال على هذا الطلب، يمكنك الاطّلاع على رمز JavaScript الخاص بالبائع في تطبيق الاختبار المحلي.

المزاد المختلط

لتنفيذ مزاد مختلط الوضع يمكن أن يشارك فيه كلّ من المشترين في "العروض حسب الجمهور" و"العروض حسب التطبيق"، يتم إنشاء إعدادات المزاد في طلب runAdAuction() على النحو التالي:

await navigator.runAdAuction({
  seller: 'https://ssp-mix.example',
  decisionLogicURL: 'https://ssp-mix.example/score-ad.js',
  componentAuctions: [
    // B&A auction result
    {
      seller: 'https://ssp-mix.example',
      requestId,
      serverResponse: protectedAudienceAuctionResult,
    },
    // On-device auction config
    {
      seller: 'https://ssp-mix.example',
      decisionLogicURL: 'https://ssp-mix.example/on-device-score-ad.js',
      interestGroupBuyers: [
        'https://dsp-a.example', // On-device buyer
        'https://dsp-a.example', // On-device buyer
      ],
    },
  ]
});

لتسهيل إجراء مزاد مختلط، يتم تمرير نتيجة مزاد "عروض الأسعار والمزايدة" وإعدادات المزاد على الجهاز إلى الحقل componentAuctions. في مزاد الوضع المختلط، تكون قيمة seller هي نفسها لكلّ من الإعدادات على أعلى مستوى وإعدادات المكوّنات.

للاطّلاع على مثال على هذا الطلب، يمكنك الاطّلاع على رمز JavaScript الخاص بالبائع في تطبيق الاختبار المحلي.

مزاد البائعين المتعدّدين

إذا كنت بائعًا على أعلى مستوى وتدير مزادًا متعدد البائعين يتم تنظيمه على الجهاز، يرسل كل بائع من البائعين المكوّنين نتيجة مزاد B&A وإعدادات مزاد الجهاز.

await navigator.runAdAuction({
  seller: 'https://ssp-top.example',
  decisionLogicURL: 'https://ssp-top.example/score-ad.js',
  componentAuctions: [
    // SSP-BA's B&A-only auction result
    {
      seller: 'https://ssp-ba.example',
      requestId: 'g8312cb2-da2d-4e9b-80e6-e13dec2a581c',
      serverResponse: Uint8Array(560) [193, 120, 4, ] // Encrypted B&A auction result
    },
    // SSP-MIX's B&A auction result
    {
      seller: 'https://ssp-mix.example',
      requestId: 'f5135cb2-da2d-4e9b-80e6-e13dec2a581c',
      serverResponse: Uint8Array(560) [133, 20, 4, ] // Encrypted B&A auction result
    }.
    // SSP-MIX's on-device auction config
    {
      seller: 'https://ssp-mix.example',
      interestGroupBuyers: ['https://dsp-a.example', 'https://dsp-b.example'],
      decisionLogicURL: 'https://ssp-mix.example/score-ad.js',
    }
    // SSP-OD's on-device auction config
    {
      seller: 'https://ssp-od.example',
      interestGroupBuyers: ['https://dsp-a.example', 'https://dsp-b.example'],
      decisionLogicURL: 'https://ssp-od.example/score-ad.js',
    }
  ]
})

للاطّلاع على مثال على هذا الطلب، يمكنك الاطّلاع على رمز JavaScript الخاص بالبائع في تطبيق الاختبار المحلي.

الخطوات التالية

بعد قراءة هذا الدليل، يمكنك اتّخاذ الخطوات التالية:

مزيد من المعلومات

هل لديك أسئلة؟