メインコンテンツまでスキップ

チュートリアル:アプリアナリティクス収集

📚 Source Page

JUCEアプリケーションでユーザーからアプリ使用データを収集します。アナリティクスモジュールを使用してGoogle Analyticsにアナリティクスイベントを送信します。

レベル: 中級
プラットフォーム: Windows, macOS, Linux, iOS, Android
クラス: ThreadedAnalyticsDestination, ButtonTracker, WebInputStream, CriticalSection, CriticalSection::ScopedLockType

警告

このプロジェクトにはGoogle Analyticsアカウントが必要です。サポートが必要な場合は、Google Analyticsウェブサイトの指示に従ってアカウントを開設してください。

はじめに

このチュートリアルのデモプロジェクトをこちらからダウンロードしてください:PIP | ZIP。プロジェクトを解凍し、最初のヘッダファイルをProjucerで開いてください。

この手順についてサポートが必要な場合は、チュートリアル:Projucer Part 1: Projucerを始めようを参照してください。

このチュートリアルが完全に機能するように、Google Analytics APIキーを書き留めて準備しておいてください。

デモプロジェクト

デモプロジェクトは、押されたときにアナリティクスイベントを送信する2つのボタンを持つ非常にシンプルなUIを表示します。APIキーがまだ設定されていないため、実装前はGoogleはイベントを受信しません。

デモプロジェクトのアプリウィンドウ
デモプロジェクトのアプリウィンドウ
ヒント

このプロジェクトはGoogle Analyticsを使用してアプリアナリティクスを追跡しますが、使用したい他のサービスにも適用できます。ここで紹介するコードは、JUCE ExamplesのAnalyticsCollectionと大まかに類似しています。

イベントの構造

イベントは、アプリケーション内でユーザーがコンテンツとどのようにインタラクションしたかを説明し、アナリティクス追跡システムに送信されます。インタラクションをより良く分類およびフィルタリングするために、イベントは以下のキーワードを使用して構造化されています:

  • カテゴリ:アナリティクスレポートの下で結合されるイベントのグループを説明します。
  • アクション:イベントをトリガーするために実行されたアクションを指定します。
  • ラベル:ユーザーとインタラクションした特定のオブジェクトを説明する追加情報。
  • 値:問題のイベントに数値データを提供するためのOptional整数。

すべてのイベントは、上記のキーワードとともに一意のユーザーIDとタイムスタンプとともに送信されます。さらに、ユーザーは、ベータテスターや開発者などの能力をより良く説明するためにカテゴリにグループ化できます。

APIキーのセットアップ

プロジェクトが正しく機能するための最初のステップは、Google Analytics APIキーをセットアップすることです。Google Analyticsダッシュボードでトラッキングを見つけることができます:

Google AnalyticsトラッキングID
Google AnalyticsトラッキングID

このIDをコピーし、GoogleAnalyticsDestinationクラスのapiKeyプレースホルダー変数を置き換えます:

apiKey = "UA-XXXXXXXXX-1";
警告

理想的には、このAPIキーはバイナリ配布物に表示されるべきではありません。発見された場合、悪意のある使用があり、スパムでアナリティクスデータを汚染する可能性があるためです。これを防ぐ1つの方法は、実行時に動的にAPIキーを取得することです(たとえば、独自のサーバーから)。

アプリ起動の追跡

まず、アプリの起動などのユーザーに依存しない情報を追跡し、アナリティクスシステムによって使用される定数ユーザー情報を定義することから始めましょう。MainContentComponentクラスのコンストラクタで、Analytics::getInstance()を呼び出してAnalyticsシングルトンへの参照を取得することから始めます。

次に、このユーザーの一意の識別子を選択してsetUserID()でユーザーIDを設定できます [1]。この識別子には機密性の高い個人情報を含めないようにしてください。また、StringPairArrayを使用してsetUserProperties()を呼び出すことで、このユーザーにユーザーグループを設定することもできます [2]。

イベントを受信するには、Analyticsインスタンスに少なくとも1つの送信先を指定する必要があります。希望すれば、オプションで複数の送信先を追加できます。この場合、GoogleAnalyticsDestinationクラスのインスタンスをシングルトンに追加します [3]。

MainContentComponentコンストラクタはMainWindowがインスタンス化されるときに呼び出されるため、コンポーネントがMainWindowに所有されたときにlogEvent()関数を使用してこのイベントをログに記録できます [4]。

MainContentComponent()
{
// ユーザーのアナリティクス識別子を追加します。許可を求めていない場合、
// 誤って識別可能な情報を収集しないように注意してください!
juce::Analytics::getInstance()->setUserId ("AnonUser1234"); // [1]

// その他の定数ユーザー情報を追加します。
juce::StringPairArray userData;
userData.set ("group", "beta");
juce::Analytics::getInstance()->setUserProperties (userData); // [2]

// 使用したいアナリティクス送信先をAnalyticsシングルトンに追加します。
juce::Analytics::getInstance()->addDestination (new GoogleAnalyticsDestination()); // [3]

// ここでのイベントタイプは、より高度なアプリでは
// おそらくDemoAnalyticsEventTypes::sessionStartであるべきです。
juce::Analytics::getInstance()->logEvent ("startup", {}, DemoAnalyticsEventTypes::event); // [4]

同様に、MainWindowが削除されたときにMainContentComponentデストラクタでシャットダウンイベントをログに記録できます [5]。

~MainContentComponent() override
{
// ここでのイベントタイプは、より高度なアプリでは
// おそらくDemoAnalyticsEventTypes::sessionEndであるべきです。
juce::Analytics::getInstance()->logEvent ("shutdown", {}, DemoAnalyticsEventTypes::event); // [5]
}

ボタン動作の追跡

特定のユーザーアクションに追跡を追加するには、どのユーザーインタラクションを記録して送信するかを定義する必要があります。幸いなことに、ボタンの動作を記録するには、JUCEアナリティクスモジュールに含まれているButtonTrackerという便利なクラスを使用でき、これが自動的に処理してくれます。

まず、MainContentComponentクラスのメンバー変数としてButtonTrackerを宣言しましょう [1]。

juce::TextButton eventButton { "Press me!" }, crashButton { "Simulate crash!" };
std::unique_ptr<juce::ButtonTracker> logEventButtonPress; // [1]

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

MainContentComponentコンストラクタで、追跡したい特定のTextButtonオブジェクトをButtonTrackerコンストラクタに引数として渡すことでリンクできます。また、イベントが発火されたときに送信するイベントカテゴリとアクションプロパティも設定します [2]。

juce::StringPairArray logButtonPressParameters;
logButtonPressParameters.set ("id", "a");
logEventButtonPress.reset (new juce::ButtonTracker (eventButton, "button_press", logButtonPressParameters)); // [2]
}
注記

演習:追加のGUIコンポーネントを作成し、異なるイベントパラメータで追跡を実装してください。

イベントの送信

JUCEアナリティクスモジュールは、専用スレッドでのイベントのログを処理し、アナリティクスデータを定期的にバッチで送信します。したがって、データが送信されるまでイベントをローカルストレージに一時的に保存する必要があります。このチュートリアルの残りの部分では、GoogleAnalyticsDestinationクラスで作業します。

まず、アプリケーションデータディレクトリにアナリティクスイベントデータを保存する場所を指定する必要があります。これには、特別な場所File::userApplicationDataDirectoryを使用して正しい場所を見つけ、アプリの対応するアプリケーションフォルダに移動します [1]。場所が存在しない場合はフォルダを作成し [2]、ファイルパスをXMLファイル名拡張子として保存します [3]。

これで、startAnalyticsThread()関数を使用してスレッドを開始し、イベントのバッチ間の待機時間をミリ秒単位で指定できます [4]。

GoogleAnalyticsDestination()
: ThreadedAnalyticsDestination ("GoogleAnalyticsThread")
{
{
// 未送信イベントを保存する場所を選択します。

auto appDataDir = juce::File::getSpecialLocation (juce::File::userApplicationDataDirectory)
.getChildFile (juce::JUCEApplication::getInstance()->getApplicationName()); // [1]

if (!appDataDir.exists())
appDataDir.createDirectory(); // [2]

savedEventsFile = appDataDir.getChildFile ("analytics_events.xml"); // [3]
}
startAnalyticsThread (initialPeriodMs); // [4]
}

クラスデストラクタでは、オペレーティングシステムによってアプリケーションが強制終了されることなく、最後のバッチイベントが送信できるようにする必要があります。これを許可するために、スレッドを強制的に停止する前に1秒後にスレッドをスリープさせながら最後のバッチ期間を提供します。これにより、アプリケーションのシャットダウン時間を過度に延長することなく、最後の送信試行に十分な時間を提供します。

~GoogleAnalyticsDestination() override
{
// ここでスリープして、バックグラウンドスレッドが
// 最後のバッチイベントを送信する機会を与えます。注意 - アプリの
// シャットダウンに時間がかかりすぎると、一部のオペレーティングシステムは
// 強制的に終了します!
juce::Thread::sleep (initialPeriodMs); // [5]

stopAnalyticsThread (1000); // [6]
}

バッチで送信するイベントの最大数は、getMaximumBatchSize()関数をオーバーライドすることで指定できます:

int getMaximumBatchSize() override { return 20; }

HTTPリクエストのフォーマット

次に、これらのイベントをアナリティクスサーバーにログ記録するための正しいHTTPリクエストをフォーマットする必要があります。例えば、ボタン押下動作の場合に構築しようとしているURLとその対応するPOSTデータは以下のようになります:

POST /batch HTTP/1.1
Host: www.google-analytics.com

v=1 // バージョン番号
&aip=1 // IPを匿名化
&tid=UA-XXXXXXXXX-1 // トラッキングID
&t=event // ログタイプ
&ec=button_press // イベントカテゴリ
&ea=a // イベントアクション
&cid=AnonUser1234 // ユーザーID
  • [v]:バッチログAPIバージョン。
  • [aip]:送信者のIPアドレスが匿名化されます。
  • [tid]:対応するアプリのトラッキングID。
  • [t]:アナリティクスシステムのログのタイプ。
  • [ec]:ログされたイベントのカテゴリ識別子。
  • [ea]:ログされたイベントのアクション識別子。
  • [cid]:対応するユーザーのユーザーID。

典型的なアプリのライフサイクルでは、バッチロガーはアプリケーションが起動したときにまずappStartedイベントを処理します。次に、ユーザーがボタンをクリックすると、button_pressイベントをログに記録し、最後にアプリケーションが終了するときにappStoppedイベントをログに記録します。

これら3つのログシナリオに対応するために、logBatchedEvents()関数で異なるリクエストを構築する必要があります:

bool logBatchedEvents (const juce::Array<AnalyticsEvent>& events) override
{
// イベントをGoogle Analyticsに送信します。

juce::String appData ("v=1&aip=1&tid=" + apiKey); // [1]

juce::StringArray postData;

for (auto& event : events) // [2]
{
juce::StringPairArray data;

switch (event.eventType)
{
case (DemoAnalyticsEventTypes::event):
{
data.set ("t", "event");

if (event.name == "startup")
{
data.set ("ec", "info");
data.set ("ea", "appStarted");
}
else if (event.name == "shutdown")
{
data.set ("ec", "info");
data.set ("ea", "appStopped");
}
else if (event.name == "button_press")
{
data.set ("ec", "button_press");
data.set ("ea", event.parameters["id"]);
}
else if (event.name == "crash")
{
data.set ("ec", "crash");
data.set ("ea", "crash");
}
else
{
jassertfalse;
continue;
}

break;
}

default:
{
// 不明なイベントタイプです!このデモアプリでは単一の
// イベントタイプのみを使用していますが、実際のアプリでは
// おそらく複数を処理したいでしょう。
jassertfalse;
break;
}
}

data.set ("cid", event.userID); // [3]

juce::StringArray eventData;

for (auto& key : data.getAllKeys()) // [4]
eventData.add (key + "=" + juce::URL::addEscapeChars (data[key], true));

postData.add (appData + "&" + eventData.joinIntoString ("&")); // [5]
}

auto url = juce::URL ("https://www.google-analytics.com/batch")
.withPOSTData (postData.joinIntoString ("\n")); // [6]
  • [1]:まず、バージョン番号、匿名化IP、トラッキングIDをappData文字列変数に追加します。
  • [2]:次に、バッチ内の各イベントについて、問題のイベントのタイプを判断してカテゴリとアクションプロパティを設定します。イベントが起動またはシャットダウンの場合、イベントカテゴリを「info」に設定し、アクションプロパティをそれぞれ「appStarted」または「appStopped」に設定します。イベントがボタン押下の場合、イベントカテゴリを「button_press」に設定し、ButtonTrackerのidパラメータからアクションプロパティを取得します。
  • [3]:また、ログするイベントのユーザーIDを設定します。
  • [4]:次に、すべての個別のStringPairArrayエントリについて、等号を挿入してキーを対応する値と連結し、URLから特殊文字をエスケープします。
  • [5]:最後に、すべてのイベントパラメータをアンパサンド記号で結合し、最初のappDataコンテンツを先頭に付加します。
  • [6]:URLは最終的にPOSTデータが行ごとに追加されて構築されます。これにより、単一のHTTPリクエストで複数のイベントを送信できます。
注記

演習:ラベルと値の属性を含むすべてのイベントプロパティを処理するように上記のコードを変更してください。

URLの準備ができたので、WebInputStreamを作成してサーバーにリクエストを送信する必要があります。まず、webStreamCreationというメンバー変数として宣言されたCriticalSectionミューテックスをロックする必要があります。ScopedLockオブジェクトを使用すると、中括弧で区切られたコード部分のミューテックスを自動的にロックおよびアンロックできます [1]。

アプリケーションが終了したためにstopLoggingEvents()関数が以前に呼び出された場合、WebInputStreamを初期化しようとせずにすぐに戻ります [2]。そうでなければ、以前に構築されたURLを引数として渡し、メソッドとしてPOSTを使用してstd::unique_ptrに作成できます [3]。

次に、指定されたURLに接続し、WebInputStreamconnect()関数を使用してリクエストを実行できます [4]。応答が成功した場合、関数から正常に戻ります。そうでなければ、前のレートに2を掛けてバッチ期間に指数減衰を設定し、関数から負の値を返します [5]。

{
const juce::ScopedLock lock (webStreamCreation); // [1]

if (shouldExit) // [2]
return false;

webStream.reset (new juce::WebInputStream (url, true)); // [3]
}

auto success = webStream->connect (nullptr); // [4]

// 接続に失敗した場合、指数バックオフを行います。
if (success)
periodMs = initialPeriodMs;
else
periodMs *= 2;

setBatchPeriod (periodMs); // [5]

return success;
}

アプリケーションがシャットダウンするとき、同時に実行されているWebInputStreamへの接続がある場合はキャンセルする必要があります。同じCriticalSectionオブジェクトからScopedLockを使用してロックを最初に取得することで、logBatchedEvents()関数で以前に遭遇したコードのクリティカルセクションが終了する前に終了することを保証します [1]。shouldExitブール値をtrueに設定すると、その後新しい接続が作成されるのを防ぎます [2]。次に、WebInputStream接続がある場合はcancel()関数を使用してキャンセルできます [3]。

void stopLoggingEvents() override
{
const juce::ScopedLock lock (webStreamCreation); // [1]

shouldExit = true; // [2]

if (webStream.get() != nullptr) // [3]
webStream->cancel();
}

これでイベントのログに関するチュートリアルの部分が完了します。ただし、イベントデータの送信に失敗してアプリケーションが終了した場合、現在は未ログイベントを追跡する方法がありません。

未ログイベントの保存と復元

このセクションでは、接続が失われた場合に未ログイベントをディスクに保存するためのXMLファイルの使用について説明します。

未ログイベント情報を保存するXMLドキュメントは、単一のボタン押下の場合、以下のようになります:

<?xml version="1.0" ?>
<events> // ドキュメント全体のルートXML要素。
<google_analytics_event
name="button_press"
type="event"
timestamp="xxxx"
user_id="AnonUser1234"
> // 名前、タイプ、タイムスタンプ、ユーザーIDを持つイベントノード。
<parameters id="a" /> // 親イベントに関連するパラメータ。
<user_properties group="beta" /> // 親イベントのユーザーのプロパティ。
</google_analytics_event>
//...
</events>

イベントの保存と復元をそれぞれ処理するsaveUnloggedEvents()restoreUnloggedEvents()関数を見てみましょう。saveUnloggedEvents()関数は、上記の形式に基づいてXML構造を構築し、内容をXMLファイルに保存します:

void saveUnloggedEvents (const std::deque<AnalyticsEvent>& eventsToSave) override
{
// 未送信イベントをディスクに保存します。ここではシリアライゼーション形式として
// XMLを使用していますが、restoreUnloggedEventsメソッドがディスクから
// イベントを復元できる限り、他の何でも使用できます。非常に多くの
// イベントを保存する場合、より高速であればバイナリ形式の方が
// 適しているかもしれません - このメソッドはアプリのシャットダウン時に
// 呼び出されるため、迅速に完了する必要があることを覚えておいてください!

juce::XmlDocument previouslySavedEvents (savedEventsFile);
std::unique_ptr<juce::XmlElement> xml (previouslySavedEvents.getDocumentElement()); // [1]

if (xml.get() == nullptr || xml->getTagName() != "events") // [2]
xml.reset (new juce::XmlElement ("events"));

for (auto& event : eventsToSave)
{
auto* xmlEvent = new juce::XmlElement ("google_analytics_event"); // [3]
xmlEvent->setAttribute ("name", event.name);
xmlEvent->setAttribute ("type", event.eventType);
xmlEvent->setAttribute ("timestamp", (int) event.timestamp);
xmlEvent->setAttribute ("user_id", event.userID);

auto* parameters = new juce::XmlElement ("parameters"); // [4]

for (auto& key : event.parameters.getAllKeys())
parameters->setAttribute (key, event.parameters[key]);

xmlEvent->addChildElement (parameters);

auto* userProperties = new juce::XmlElement ("user_properties"); // [5]

for (auto& key : event.userProperties.getAllKeys())
userProperties->setAttribute (key, event.userProperties[key]);

xmlEvent->addChildElement (userProperties);

xml->addChildElement (xmlEvent); // [6]
}

xml->writeTo (savedEventsFile); // [7]
}
  • [1]:まず、以前に定義されたファイルの場所に保存されたXMLファイルから以前に保存されたイベントを取得し、それに基づいてXmlElementを構築します。
  • [2]:XmlElementが存在しないか、ルート「events」ノードを持たない場合、作成します。
  • [3]:キュー内の各未保存イベントについて、イベント名、タイプ、タイムスタンプ、ユーザーIDを属性として持つ「google_analytics_event」ノードを作成します。
  • [4]:また、以前に作成したノードの子ノードとして、イベントパラメータを属性として持つ「parameters」ノードを作成します。
  • [5]:同じ階層レベルで、ユーザープロパティを属性として持つ「user_properties」ノードを子ノードとして作成します。
  • [6]:次に、個々のイベントノードをルート「events」ノードの子として追加できます。
  • [7]:最後に、XML構造をXMLファイルに書き込み、イベントを保存します。

一方、restoreUnloggedEvents()関数は、以前に示したのと同じ形式に基づいてXML構造を読み取り、イベントキューを埋めます:

void restoreUnloggedEvents (std::deque<AnalyticsEvent>& restoredEventQueue) override
{
juce::XmlDocument savedEvents (savedEventsFile);
std::unique_ptr<juce::XmlElement> xml (savedEvents.getDocumentElement()); // [1]

if (xml.get() == nullptr || xml->getTagName() != "events") // [2]
return;

auto numEvents = xml->getNumChildElements();

for (auto iEvent = 0; iEvent < numEvents; ++iEvent)
{
auto* xmlEvent = xml->getChildElement (iEvent); // [3]

juce::StringPairArray parameters;
auto* xmlParameters = xmlEvent->getChildByName ("parameters"); // [4]
auto numParameters = xmlParameters->getNumAttributes();

for (auto iParam = 0; iParam < numParameters; ++iParam)
parameters.set (xmlParameters->getAttributeName (iParam),
xmlParameters->getAttributeValue (iParam));

juce::StringPairArray userProperties;
auto* xmlUserProperties = xmlEvent->getChildByName ("user_properties"); // [5]
auto numUserProperties = xmlUserProperties->getNumAttributes();

for (auto iProp = 0; iProp < numUserProperties; ++iProp)
userProperties.set (xmlUserProperties->getAttributeName (iProp),
xmlUserProperties->getAttributeValue (iProp));

restoredEventQueue.push_back ({ xmlEvent->getStringAttribute ("name"), // [6]
xmlEvent->getIntAttribute ("type"),
static_cast<juce::uint32> (xmlEvent->getIntAttribute ("timestamp")),
parameters,
xmlEvent->getStringAttribute ("user_id"),
userProperties });
}

savedEventsFile.deleteFile(); // [7]
}
  • [1]:以前と同様に、以前に定義されたファイルの場所に保存されたXMLファイルから以前に保存されたイベントを取得し、それに基づいてXmlElementを構築します。
  • [2]:XmlElementが存在しないか、ルート「events」ノードを持たない場合、何もすることがないので関数から戻ります。
  • [3]:まず、ルート親からパースする単一のイベント子ノードを取得します。
  • [4]:子「parameters」ノードの各属性について、キー/値ペアを設定し、StringPairArrayに追加します。
  • [5]:子「user_properties」ノードの各属性について、キー/値ペアを設定し、StringPairArrayに追加します。
  • [6]:次に、StringPairArrayオブジェクトから対応するパラメータを設定することで、個々のイベントをイベントキューにプッシュバックできます。
  • [7]:最後に、完了したらディスクからXMLファイルを削除します。
ヒント

シリアライゼーション形式としてXMLを使用しましたが、大量の未保存イベントを保存する必要がある場合は、バイナリ形式の方が効率的です。

注記

演習:未ログイベントをJSONなどの異なるシリアライゼーション形式またはバイナリ形式で保存および復元してください。

まとめ

このチュートリアルでは、Google AnalyticsとJUCEアナリティクスモジュールを使用して使用データを追跡する方法を学びました。特に、以下のことを行いました:

  • 別のスレッドでGoogle Analyticsにアナリティクスイベントを送信しました。
  • 未送信イベントをXMLドキュメントにローカルで保存しました。
  • XMLドキュメントから保存されたイベントをイベントキューに復元しました。

関連項目