インセンティブ広告設定

ユーザー体験を維持しつつ、収益を最大化するため、Vungleはオプトイン・インタースティシャル広告の設定を推奨します。アプリの自然な途切れ目に表示されるオプトイン・インタースティシャル広告は、より高い収益を生むため、スキップ不可に設定することを推奨します。ユーザーはビデオの視聴を選択し、バーチャル通貨、プレミアムコンテンツ、アプリ内の商品など価値のあるものを得ることができるので、この広告配置は素晴らしいUXを提供します。ビデオ視聴完了でユーザーに与えられるリワード額と種類は、すべてパブリッシャー様次第です。

実行するには2つの方法があります:

1. アプリケーション内報酬

概要

こちらはサーバー間コールバックに代わってご使用いただけます。ユーザーがビデオ視聴を完了するとアプリ内でユーザーにリワードを提供されます。

ここでの主な利点は、実行がとても簡単ということです。

実行 - iOS

詳細設定ガイドのDelegate Methodsの下にあるVungle SDK Delegateを実行してください。

このコールバック(void)vungleSDKwillCloseAdWithViewInfo:が、viewInfoディクショナリーを送ります。

completedViewキーがYESを返す場合、表示したユーザーに報酬を提供します。

実行 - Android

詳細設定ガイドにあるEventListenerインターフェースを実行してください。

isCompletedViewonVideoView内の)がtrueを返す場合、ユーザーは報酬を獲得します。

 

2. サーバー間コールバック(推奨)

概要

サーバー間コールバックにより、広告を視聴したユーザーにゲーム内の通貨またはその他の報酬を提供できます。ユーザーが広告の視聴を完了すると、Vungleのサーバーからパブリッシャー様のサーバーまでコールバックが行われ、ユーザーの動作の完了通知を受けることができます。

ここでの利点はコントロールです。このメソッドにより、サーバー側の変更やアップデートを直接行うことができます。したがって、アップデートをプッシュする必要がありません。もう一つの利点は、セキュリティで、リプレイアタック(有効なデーターの遷移が悪意を持ってまたは不正に繰り返されたり遅らせられたりする)を防止します。

実行

基本設定

iOSの場合、ここで最初にしなければならいことは、playAdオプションの一つであるvunglePlayAdOptionKeyIncentivizedを

YESに設定することです。他にも関連playAdオプションが、ここにあります。

Androidの場合、最初にしなければならいことは、AdConfigオブジェクトのsetIncentivizedを

trueに設定することです。他にも関連playAdオプションが、ここにあります。

ユーザーが、インセンティブ広告の80%以上を視聴すると、視聴完了したと見なされます。その後、Vungleは、以下のようなコールバックURLをアプリのサーバーにPINGします。

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

または、

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

ダッシュボード上のアプリ設定にコールバックURLを作成します(下図参照)。

 

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

変数説明
%user% iOS - playAd()に送られる options dictVunglePlayAdOptionKeyUserキー
Android - playAd()に送られるglobal ad config objectsetIncentivizedUserIdセッター
%udid% ユニークデバイスID
%ifa% iOS: AppleのユニークデバイスID。Android:Google Advertiser ID。
%txid% 視聴完了に対するユニークID
%digest% コールバックがVungleから出たことを確認するためのセキュリティートークン。 詳細は、セキュリティーのセクションをご参照ください。

送る必要があるヴァリアブルは、%user%のみですのでご注意ください。ほかのヴァリアブルをコールバックURLに入れても、Vungleのサーバーから戻ってきます。

リワード・コンフィグレーション

リワード・コンフィグレーションで広告を視聴したユーザーに報酬を提供できます。報酬設定のビルドインオプションはありませんが、以下のようなリワードを推奨します。

例 - コイン vs. ライフ:

アプリでは、複数個所(ショップや3回目のゲームが終わるごと)にインセンティブ広告が表示されるとしましょう。ショップではコインを、ゲーム終了時にはライフという形で報酬を与えることが可能です。playAd()のインスタンスごとに、以下のようなuserをコンフィグしてください。

userName123:coins または userName123:lives

その後、サーバーがVungleのコールバックを受け取ると、%user%の構文解析を行い、報酬が正しいのかを判断します。

セキュリティー

コールバックを認証する

詳細設定の中で、受け取るコールバックがVungle起点のコールバックであることを確認するため、アプリのSecure CallbackチェックボックスのSecret Keyを選択します。これにより、以下のようなシークレットキーが作成されます。

4YjaiIualvm8/4wkMBRH8pctlqB1NyzhK3qUGUar+Zc=

 

 

コールバックの起点は以下のようにご確認いただけます。

  1. コロン(:)で区切られたtransaction IDにシークレットキーを連結させ、raw transaction verification stringを作成します: transactionString = secretKey + ":" + %txid%
  2. SHA-256アルゴリズムを使い、transactionStringのバイトを2回ハッシュします。
  3. SHA-256ハッシュ処理の2連続ラウンドのアウトプットバイトをhex-encodingに変換して、以下のような処理確認トークンを作成します。transactionToken = 870a0d13da1f1670b5ba35f27604018aeb804d5e6ac5c48194b2358e6748e2a8
  4. 作成したtransactionTokenが、コールバックquerystringで%digest%として送られたものと同じであることを確認します。

リプレイアタックを防止する

サーバーに対し、1つのコールバックが複数回リプレイされるのを防止するには、認証されたtransaction IDを保存し、複製したtransaction IDで将来のコールバックを拒否します。transation IDにはタイムスタンプが含まれているため、以下のとおり、タイムリーなコールバックを切断することで、複製のために保存し確認しなければならないtransaction IDの数を制限できます。

  1. タイムスタンプ(ミリ秒)をtransaction IDから抽出します: transactionMillis = transactionId.substringAfter(":")
  2. transactionMillisが切断より遅いことと、その切断以降transactionIdが引き起こされていないことを確認します。

サンプルコード

これらサンプルコードのビットは、サーバー間コールバックにセキュリティーを入れ込むのに役立ちます。Node.js、Java、Python、Ruby

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) {
  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
    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."""
  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

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
他にご質問がございましたら、リクエストを送信してください

コメント