報酬対象の広告の設定

コンテンツ

オプトインのインタースティシャル広告を使用する理由

優れたユーザー エクスペリエンスを維持しながら収益を最大化するため、Vungle では、オプトイン インタースティシャルの使用を推奨しています。アプリの自然な切り替えポイントで表示されるオプトイン インタースティシャル広告は、特に Vungle のおすすめに従ってスキップ不可にした場合、高い収益を生み出します。また、この広告配置は、ユーザーが動画の視聴を選択し、仮想通貨、プレミアム コンテンツ、アプリ内商品など価値のあるものという形で報酬を受けることができるため、優れたユーザー エクスペリエンスも提供します。動画を最後まで視聴することでユーザーに与えられる報酬の量や種類は、提供者が自由に決めることができます。これを行うには、アプリ内報酬またはサーバー間コールバックの 2 つの方法があります。

推奨オプション: アプリ内報酬

概要

これは、サーバー間コールバックに代わる方法です。ユーザーが広告の視聴を終えるか、ダウンロード ボタンをクリックしたときに、アプリの中で直接報酬を与えることができます。このアプローチの主な利点は、実装がかなり簡単であるということです。手間のかからない方法を探していて、リプレイ アタックの心配がないのであれば、この方法で十分でしょう。

Vungle は、Dynamic Template 広告を使ったさまざまな広告形式を提供するようになりました。広告再生に続いてエンドカードが表示される動画再生で構成されていた従来の広告形式とは違い、動画再生中にコールトゥアクション (CTA) ボタンが利用できるテンプレートを提供しています。動画広告を最後まで視聴したユーザーと同様に、ボタンをクリックしたユーザーにも報酬を与える必要があります。

注意: 報酬対象 の広告は、 インセンティブ 広告として参照されることがあります。どちらの場合も、同等な広告のことを示しています。SDKのコードと Reporting API では、「インセンティブ」を使用します。

iOS 向けの実装

「Vungle スタート ガイド - iOS SDK v. 5.1 +」の "デリゲート コールバック" セクションで説明されている VungleSDK Delegate を実装します。

- (void)vungleWillCloseAdWithViewInfo:(VungleViewInfo *)info placementID:(NSString *)placementID;

コールバック vungleSDKwillCloseAdWithViewInfo が、completedView または didDownload のキーに対して値が 'yes' の viewInfo ディクショナリーを渡してきた場合、ユーザーは報酬を獲得しています。

Android 向けの実装

Vungle スタート ガイド - Android SDK v. 5.1 +」で説明されている EventListener インターフェイスを実装します。

public void onAdEnd (String placementReferenceId, boolean wasSuccessfulView, boolean wasCallToActionClicked)

コールバック onAdEndwasSuccessfulView または wasCallToActionClicked に対して ‘true’ を返した場合、ユーザーは報酬を獲得しています。

サーバー間コールバック

概要

サーバー間コールバックを使用すると、広告を視聴したユーザーに、ゲーム内通貨やその他の報酬による報酬を与えることができます。ユーザーが広告を最後まで視聴したときに、ユーザーが操作を完了したことを知らせるため、Vungle のサーバーから自分のサーバーへのコールバックを設定することができます。

このアプローチの利点の一つはコントロールです。この方法では、サーバー側で変更と更新を行うことができるため、更新をプッシュする必要がありません。もう一つの利点として、リプレイ アタック (有効なデータ転送が故意または不正に繰り返されたり遅らされたりすること) を防止する上でのセキュリティがあります。

実装

Vunble SDK v. 5.x に関する注意: SDK v. 5.1 以降、報酬オプションをダッシュボードに移動しました。これにより、パブリッシャーは、コードを変更することなく簡単にオプションを変更できます。ダッシュボードで、 [Application Stage][Placements Stage] → 鉛筆アイコンをクリック image1.png → [Edit Placement] まで移動し、広告配置レベルでの報酬オプションを有効化または無効化するためのチェックボックスを探します。

image2.jpg

Vungle SDK v.1.0 - v.4.1 に関する注意:

  • iOS の場合、まず playAd オブジェクトで vunglePlayAdOptionKeyIncentivized オプションを 'yes' に設定します(このオプションと関連する playAd オプションの詳細についてはこちらをご覧ください)。

  • Android の場合、まず AdConfig オブジェクトで setIncentivized オプションを 'true' に設定します(このオプションと関連する playAd オプションの詳細についてはこちらをご覧ください)。

すべての Vungle SDKバージョンの実装

報酬対象の広告の 80% 以上を視聴すると、視聴を完了したと見なされます。そこで Vungle がコールバック URL を使用してサーバーに ping を実行します。その内容は次のようになります。

http://acme.com/bugzBunny/reward?uid=%user%

または次のようになります。

http://acme.com/bugzBunny/reward? amount=1&uid=%user%&txid=%txid%&digest=%digest%

ここではetxid およびedigestを使用した次の例を推奨します。

http://acme.com/bugzBunny/reward? amount=1&uid=%user%&etxid=%etxid%&edigest=%edigest%.

以下に示すように、ダッシュボードのアプリの [Settings] でコールバック URL を設定します。

CallbackURL.gif

ほとんどのパブリッシャーは、セキュリティのため %user%plus%txid%%digest% のみを使用しますが、以下のすべてが使用できます。

変数

説明

%user%

次を通して Vungle SDK に提供されるユーザー名:

●      iOS: playAd() に渡されるオプション ディクショナリーVunglePlayAdOptionKeyUser キー

●      Android: playAd() に渡されるグローバル広告設定オブジェクトsetIncentivizedUserId 設定ツール

%udid%

デバイスの一意の識別子

%ifa%

●      iOS: Apple のデバイスの一意の識別子

●      Android: Google Advertiser ID が返ってきます

%etxid%

視聴完了を示す一意のトランザクション ID とサーバーの時刻; transactionID:timestamp (推奨)。

%edigest%

コールバックが Vungle から来たことを確認するセキュリティ トークン; 詳細は セキュリティ のセクションを参照してください (推奨)。

%txid%

最後まで視聴された一意のトランザクション ID

%digest%

コールバックが Vungle から来たことを確認するセキュリティ トークン。 詳細はセキュリティのセクションを参照してください。

渡す必要がある変数は %user% だけです。その他の変数はコールバック URL に含めることで Vungle のサーバーから戻ってきます。

報酬の設定

広告を視聴するユーザーに報酬を与えることができるようになったら、次は何を与えるかを考える必要があります。毎回お宝を進呈しているのであれば話は簡単ですが、もう少し高度な報酬制度を導入したい場合はどうでしょうか。Vungle では、報酬設定の組み込みオプションは提供していませんが、次の設定を推奨しています。

例: コイン vs. ライフ

アプリ内の複数の場所に報酬対象の広告があると仮定します。ショップの中、そしてゲームオーバー 3 回ごとに広告が表示される設定です。ショップではコインを、ゲームオーバーではライフを報酬として与えたいと考えているとします。それぞれのplayAd()インスタンスに、ユーザーを次のように設定します:

userName123:coins or userName123:lives 

そしてサーバーが Vungle のコールバックを受け取ったら、%user%を解析し適切な報酬を判断します。

セキュリティ

コールバックの認証

受け取ったコールバックの発信元が Vungle であることを確認するには、アプリケーションの [Secret Key for Secure Callback] のチェックボックスをオンにします。これにより、次のような秘密鍵が生成されます。

4YjaiIualvm8/4wkMBRH8pctlqB1NyzhK3qUGUar+Zc=

この秘密鍵を使って、コールバックの発信元を確認する方法は次のとおりです。

  1. 秘密鍵とトランザクション IDを結合してコロンで区切り、次のような生のトランザクション検証文字列を作成します。
    transactionString = secretKey + ":" + %txid% or %etxid%
  2. SHA-256 アルゴリズムを使用して transactionString のバイトを 2 回ハッシュします。
  3. SHA-256 ハッシングの連続した 2 ラウンドの出力バイトを 16 進数でエンコードすることにより、トランザクション検証トークンを生成します。これは次のようになります。
    transactionToken = 870a0d13da1f1670b5ba35f27604018aeb804d5e6ac5c48194b2358e6748e2a8
  4. 生成したtransactionToken を確認し、てコールバック クエリ文字列で送信された次のいずれかのトークンと同じであることを確認します。%digest% または%edigest%

リプレイ アタックの防止

1 つのコールバックがサーバーに対して何度も繰り返されないようにするには、認証済みのトランザクション ID を格納し、重複したトランザクション ID を使用したコールバックを拒否します。トランザクション ID にはタイムスタンプが含まれているため、タイムリーなコールバックのカットオフを実施することにより、重複がないか確認するために保存するトランザクション ID の数を制限することができます。

  1. 次のように、トランザクション ID からタイムスタンプ (ミリ秒単位) を抽出します。
    transactionMillis = transactionId.substringAfter(":")
  2. transactionMillis がカットオフより後であり、カットオフから transactionId が検出されていないことを確認します。

サンプル コード

ここでは、サーバー間コールバックのセキュリティに役立つサンプルコードを紹介します!Node.js、Java、Python、Ruby 向けの例を用意しています。以下の点に注意してください:

  • この transaction_id%txid% または%etxid% です (推奨)。
  • また%etxid%については検証ハッシュは以下になります%edigest%.

Node.js

var crypto = require('crypto');

function isTransactionValid(secret, transaction_id, provided_hash) {
  return isTransactionRecent(transaction_id) &&
         isTransactionNew(transaction_id)    &&
         createSecurityDigest(secret, transaction_id) === provided_hash;
}

function getTransactionTimestamp(transaction_id) {
  return parseInt(transaction_id.split(":")[1], 10) || null;
}

function isTransactionRecent(transaction_id) {
  // Does the transaction have a reasonable format?
  var tx_timestamp = getTransactionTimestamp(transaction_id);
  if (tx_timestamp === null) { return false; }

  // Is the transaction within a reasonable time range?
  var now = new Date().getTime();
  var time_diff = now - tx_timestamp;
  var hour_in_future = -1000 * 60 * 60, three_days_ago = 1000 * 60 * 60 * 24 * 3;
  return (time_diff < three_days_ago && time_diff > hour_in_future);
}

// Have we seen this transaction before?
// NOTE: To scale beyond just this one node process, you'll need to put this in some
// sort of centralized datastore. Any one that supports atomic insertions into a set
// should do. The Redis database, with its SADD command, would be a good place to start.
var known_transactions = {};
function isTransactionNew(transaction_id) {

// For %etxid%, extract unique event id from etxid, which is the string before “:”, and use it to identify unique ad events
if (known_transactions[transaction_id]) { return false; } known_transactions[transaction_id] = true; return true; } function createSecurityDigest(secret, transaction_id) { var firsthash = crypto.createHash("sha256").update(secret + ":" + transaction_id).digest("binary"); return crypto.createHash("sha256").update(firsthash,"binary").digest("hex"); }

Java

import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;

public class ServerCallbackSecurityExample {
  private static final long MAX_CALLBACK_AGE_MILLIS = 7 * 24 * 60 * 60 * 1000;  // 7 days

  /**
   * Checks that a transaction is recent enough, signed with the secretKey, and not a duplicate.
   * 
   * @param transactionId the transaction ID.
   * @param secretKey a shared secret.
   * @param verificationDigest the verification digest sent in the callback querystring.
   */
  public boolean isValidTransaction(String transactionId, String secretKey, String verificationDigest) throws NoSuchAlgorithmException {
    return isRecentTransaction(transactionId)
      && isDigestValid(transactionId, secretKey, verificationDigest)
      && isNewTransaction(transactionId);
  }

  protected boolean isRecentTransaction(String transactionId) {
    boolean isRecent = false;
    try {
      final long minCallbackAgeMillis = System.currentTimeMillis() - MAX_CALLBACK_AGE_MILLIS;
      final long transactionMillis = getTransactionMillis(transactionId);
      isRecent = (transactionMillis > minCallbackAgeMillis);
    }
    catch (ParseException exception) {
      // invalid transaction ID format
    }
    return isRecent;
  }

  protected boolean isDigestValid(String transactionId, String secretKey, String verificationDigest) throws NoSuchAlgorithmException {
    return createSecurityDigest(transactionId, secretKey)
      .equals(verificationDigest);
  }

  protected boolean isNewTransaction(String transactionId) {
    // TODO if transactionId is new, store the transactionId with its associated transactionMillis
// For %etxid%, extract unique event ID from etxid, which is the string before ":", and use it to identify unique ad events
return true; } protected long getTransactionMillis(String transactionId) throws ParseException { final int transactionMillisIndex = transactionId.lastIndexOf(":") + 1; try { if (transactionMillisIndex > 0 && transactionMillisIndex < transactionId.length()) { return Long.parseLong( transactionId.substring(transactionMillisIndex)); } else { throw new ParseException("No timestamp in transaction ID", transactionMillisIndex); } } catch (NumberFormatException exception) { throw new ParseException("Invalid transaction ID timestamp", transactionMillisIndex); // invalid timestamp } } protected String createSecurityDigest(String transactionId, String secretKey) throws NoSuchAlgorithmException { final String verificationString = secretKey + ":" + transactionId; final MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); return toHexString( messageDigest.digest( messageDigest.digest( verificationString.getBytes(Charset.forName("US-ASCII"))))); } protected String toHexString(byte[] bytes) { final StringBuffer hexStringBuffer = new StringBuffer(); for (final byte byt : bytes) { hexStringBuffer.append( Integer.toString((byt & 0xff) + 0x100, 16) .substring(1)); } return hexStringBuffer.toString(); } }

Python

import hashlib, time

def isTransactionValid(secret, transaction_id, input_hash):
  """Returns whether this transaction id / hash should be considered valid"""
  return isTransactionIDRecent(transaction_id) and \
         isTransactionIDNew(transaction_id)    and \
         createSecurityDigest(secret, transaction_id) == input_hash

def getTransactionTimestamp(transaction_id):
  """Will return the unix time (in milliseconds) of this transaction, or None if invalid"""
  parsed = transaction_id.split(":")
  try:
    return int(parsed[1]) if len(parsed) == 2 else None
  except ValueError as e:
    return None

def isTransactionIDRecent(transaction_id):
  """Is this transaction within a reasonable time range?"""
  tx_time = getTransactionTimestamp(transaction_id)

  # Handle bad transaction:
  if tx_time is None:
    return False

  # Handle bad transaction times:
  now = int(time.time() * 1000)
  three_days_ago = now - (1000 * 60 * 60 * 24 * 3)
  one_hour_from_now = now + (1000 * 60 * 60)
  return ( three_days_ago < tx_time < one_hour_from_now )

def isTransactionIDNew(transaction_id, known_transaction_ids=set()):
  """Is this a duplicate transaction?
     NOTE: We only use the Python set for simplicity. For better / more centralized solutions,
     you can use any datastore that supports atomic insertion into a set. For starters, try
     Redis with its "SADD" command."""

"""For %etxid%, extract unique event ID from etxid, which is the string before ":", and use it to identify unique ad events"""
if transaction_id in known_transaction_ids: return False # Else, valid: known_transaction_ids.add(transaction_id) return True def createSecurityDigest(secret, transaction_id): """Will return the string that the security hash should have been""" firsthash = hashlib.sha256() firsthash.update(secret + ":" + transaction_id) secondhash = hashlib.sha256() secondhash.update(firsthash.digest()) return secondhash.hexdigest()

Ruby

require "openssl"
require "digest/sha2"
require "base64"
require "time"

# just some helper methods, ignore if using Rails
class Fixnum
  SECONDS_IN_DAY = 24 * 60 * 60
  HOURS_IN_DAY = 24 * 60

  def days
    self * SECONDS_IN_DAY
  end

  def hour
    self * 60 * 60
  end

  def ago
    (Time.now - self)
  end

  def from_now
    (Time.now + self)
  end
end

def transaction_valid?(secret, txid, input_hash)
  transaction_id_recent?(txid) && transaction_id_new?(txid) && create_security_digest(secret, txid) == input_hash
end

def transaction_timestamp(txid)
  arr = txid.split(":")
  return arr[1].to_i if arr.size == 2
end

def transaction_id_recent?(txid)
  tx_time = transaction_timestamp(txid)
  return false if tx_time.nil?

  now = Time.now.to_i
  three_days_ago = 3.days.ago
  one_hour_from_now = 1.hour.from_now
  three_days_ago.to_i < tx_time && tx_time < one_hour_from_now.to_i
end

# For %etxid%, extract unique event ID from etxid, which is the string before ":", and use it to identify unique ad events def transaction_id_new?(txid, transactions = []) return false if transactions.include?(txid) transactions << txid return true end def create_security_digest(secret, txid) verification_string = "#{secret}:#{txid}" first_digest = Digest::SHA2.new(256).update(verification_string) Digest::SHA2.new(256).hexdigest(first_digest.digest) end
他にご質問がございましたら、リクエストを送信してください

コメント