アプリ分析の収集
JUCEアプリケーションのユーザーからアプリの利用データを収集する。アナリティクスモジュールを使用して、アナリティクスイベントをGoogle Analyticsに送信します。
レベル:中級
プラットフォーム:Windows, macOS, Linux, iOS, Android
クラス: ThreadedAnalyticsDestination,ButtonTracker,WebInputStream,CriticalSection,CriticalSection::ScopedLockType
このプロジェクトにはGoogle Analyticsのアカウントが必要です。アカウント取得にヘルプが必要な場合はGoogle Analytics website口座を開設する。
スタート
このチュートリアルのデモ・プロジェクトのダウンロードはこちらから:PIP|ZIP.プロジェクトを解凍し、最初のヘッダーファイルをProjucerで開く。
このステップでヘルプが必要な場合は、以下を参照してください。Tutorial: Projucer Part 1: Getting started with the Projucer.
このチュートリアルが完全に機能するように、Google Analytics APIキーをメモして準備しておいてください。
デモ・プロジェクト
デモ・プロジェクトでは、2つのボタンが押されたときにアナリティクス・イベントを送信する、非常にシンプルなUIを示している。APIキーはまだ設定されていないので、実装前はGoogleはイベントを受け取らない。

このプロジェクトでは、Google Analyticsを使用してアプリの分析を追跡していますが、使用したい他のサービスにも適用できます。
ここで紹介するコードは、大まかに以下のものと似ている。アナリティクスコレクションJUCE Examplesより。
イベントの解剖学
イベントは、ユーザーがアプリケーションのコンテンツとどのようにインタラクションしたかを記述し、アナリティクスのトラッキングシステムに送信されます。インタラクションをよりよく分類し、フィルタリングするために、イベントは以下のキーワードを使って構造化されます:
- カテゴリー:分析レポートの下に組み合わされるイベントのグループを記述します。
- アクション:イベントをトリガーするために実行されたアクションを指定します。
- ラベル:ユーザーと相互作用した特定のオブジェクトを説明する追加情報。
- 価値がある:Optional当該イベントに数値データを提供する整数。
すべてのイベントは、上記のキーワードとともに、ユニークなユーザーIDとタイムスタンプとともに送信される。さらに、ベータテスターや開発者など、ユーザーの能力をよりよく説明するために、ユーザーをカテゴリーにグループ化できます。
APIキーの設定
プロジェクトが正しく動作するための最初のステップは、Google Analytics APIキーを設定することです。トラッキングIDはGoogleアナリティクスのダッシュボードにあります:

このIDをコピーしてapiKeyのプレースホルダ変数GoogleAnalyticsDestinationクラスである:
apiKey = "UA-XXXXXXXXX-1";
理想的には、このAPIキーはバイナリー・ディストリビューションでは見えないようにすべきです。発見された場合、あらゆる種類の悪意のある使い方があり、アナリティクス・データをスパムで汚染するかもしれないからです。これを防ぐ1つの方法は、実行時にAPIキーを動的に取得することです(独自のサーバーからなど)。
トラッキングアプリの起動
まず、アプリの起動などユーザーに依存しない情報を追跡することから始め、分析システムで使用される一定のユーザー情報を定義しましょう。アプリのコンストラクタでMainContentComponentクラスへの参照を取得することから始めます。Analyticsを呼び出してシングルトン化する。Analytics::getInstance().
次に、ユーザーIDをsetUserID()このユーザーに固有の識別子を選択する[1].この識別子には、機密性の高い個人情報を含めないようにしてください。また、このユーザーにユーザー・グループを設定するにはsetUserProperties()を使用している。StringPairArray [2].
イベントを受信するためには、少なくとも1つの宛先を指定する必要がある。Analyticsインスタンスに追加できます。オプションで複数の目的地を追加することもできます。この場合はGoogleAnalyticsDestinationクラスのインスタンスをシングルトンに追加します。[3]。
以来MainContentComponentコンストラクタは、MainWindowがインスタンス化されたときに呼び出されるので、このイベントをログに記録するには、関数logEvent()コンポーネントがMainWindowに所有された時[4]。
MainContentComponent()
{
// Add an analytics identifier for the user. Make sure you don't accidentally
// collect identifiable information if you haven't asked for permission!
juce::Analytics::getInstance()->setUserId ("AnonUser1234"); // [1]
// Add any other constant user information.
juce::StringPairArray userData;
userData.set ("group", "beta");
juce::Analytics::getInstance()->setUserProperties (userData); // [2]
// Add any analytics destinations we want to use to the Analytics singleton.
juce::Analytics::getInstance()->addDestination (new GoogleAnalyticsDestination()); // [3]
// The event type here should probably be DemoAnalyticsEventTypes::sessionStart
// in a more advanced app.
juce::Analytics::getInstance()->logEvent ("startup", {}, DemoAnalyticsEventTypes::event); // [4]
同様に、シャットダウンイベントをMainContentComponentMainWindowが削除されたときにデストラクタを実行する。[5]。
~MainContentComponent() override
{
// The event type here should probably be DemoAnalyticsEventTypes::sessionEnd
// in a more advanced app.
juce::Analytics::getInstance()->logEvent ("shutdown", {}, DemoAnalyticsEventTypes::event); // [5]
}
ボタン操作のトラッキング
特定のユーザーアクションにトラッキングを追加するには、どのユーザーインタラクションを記録して送信したいかを定義する必要があります。幸いなことに、ボタンの動作を記録するには、JUCE analyticsモジュールに含まれているButtonTrackerが自動的に処理してくれる。
まずButtonTrackerのメンバー変数としてMainContentComponentクラス[1]。
juce::TextButton eventButton { "Press me!" }, crashButton { "Simulate crash!" };
std::unique_ptr 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](https://docs.juce.com/master/classFile.html#a3e19cafabb03c5838160263a6e76313da0c9f89d8dc9f9f32c9eb42428385351d "The folder in which applications store their persistent user-specific settings.")で正しい場所を探し、アプリの対応するアプリケーション・フォルダーに移動する。[1].場所が存在しない場合は、フォルダを作成します。[2]そして、ファイルパスをXMLファイル名の拡張子として保存する。[3]。
スレッドを開始するにはstartAnalyticsThread()関数で、イベントのバッチ間の待ち時間をミリ秒単位で指定する。[4]。
GoogleAnalyticsDestination()
: ThreadedAnalyticsDestination ("GoogleAnalyticsThread")
{
{
// Choose where to save any unsent events.
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秒後にスレッドを強制的に停止する前に、スレッドをスリープさせている間に最後のバッチ期間を1回提供します。これにより、アプリケーションのシャットダウン時間をあまり長くすることなく、最後の送信を試みるのに十分な時間が確保される。
~GoogleAnalyticsDestination() override
{
// Here we sleep so that our background thread has a chance to send the
// last lot of batched events. Be careful - if your app takes too long to
// shut down then some operating systems will kill it forcibly!
juce::Thread::sleep (initialPeriodMs); // [5]
stopAnalyticsThread (1000); // [6]
}
をオーバーライドすることで、バッチで送信するイベントの最大数を指定できます。getMaximumBatchSize()関数は次のようになる:
int getMaximumBatchSize() override { return 20; }
HTTPリクエストのフォーマット
次に、これらのイベントをアナリティクス・サーバーに記録するために、正しいHTTPリクエストをフォーマットする必要があります。そのURL例えば、ボタンが押された場合の動作は次のようになります:
POST /batch HTTP/1.1
Host: www.google-analytics.com
v=1 // Version Number
&aip=1 // Anonymise IP
&tid=UA-XXXXXXXXX-1 // Tracking ID
&t=event // Log Type
&ec=button_press // Event Category
&ea=a // Event Action
&cid=AnonUser1234 // User ID
- [v]バッチロギングAPIのバージョン。
- [アイピー]送信者のIPアドレスは匿名化されます。
- [tid]。該当アプリのトラッキングID。
- [t]分析システムのロギングのタイプ。
- [ec]ログに記録されたイベントのカテゴリー識別子。
- ea][eaログに記録されたイベントのアクション識別子。
- [cid]対応するユーザーのユーザーID。
典型的なアプリのライフサイクルでは、バッチ化されたロガーは、アプリが起動すると、まずappStartedイベントを処理します。次に、ユーザーがボタンをクリックすると、button_pressイベントをログに記録し、最後に、アプリケーションが終了すると、appStoppedイベントをログに記録します。
これら3つのロギングシナリオを考慮するために、私たちはlogBatchedEvents()関数である:
bool logBatchedEvents (const juce::Array& events) override
{
// Send events to 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:
{
// Unknown event type! In this demo app we're just using a
// single event type, but in a real app you probably want to
// handle multiple ones.
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.
- [3]また、ログに記録するイベントのユーザーIDも設定します。
- [4]さて、個々のStringPairArrayエントリーの間に等号を挿入し、特殊文字をエスケープすることで、キーと対応する値を連結します。URL.
- [5]最後に、すべてのイベント・パラメーターをアンパサンド記号で連結し、appDataの内容を先頭に追加する。
- [6]:そのURLは最終的に、POSTデータが一行ずつ追加されて構築される。このようにして、1つのHTTPリクエストで複数のイベントを送信できる。
上記のコードを修正して、label属性とvalue属性を含むすべてのイベント・プロパティを処理する。
これでURLリクエストをサーバーに送信する必要がある。WebInputStream.まずCriticalSectionwebStreamCreationというメンバー変数として宣言されたミューテックスです。ScopedLockオブジェクトを使用すると、中かっこで区切られたコード部分に対してミューテックスを自動的にロックしたりアンロックしたりできます。[1]。
もしstopLoggingEvents()関数がアプリケーションの終了によって呼び出された場合は、アプリケーションの初期化を試みずに即座にリターンする。WebInputStream [2].そうでなければ、先に構築したURLを引数にとり、メソッドとしてPOSTを使用する。[3]。
そして、指定されたURLを使ってリクエストを実行する。connect()関数を使用する。WebInputStream [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]
// Do an exponential backoff if we failed to connect.
if (success)
periodMs = initialPeriodMs;
else
periodMs *= 2;
setBatchPeriod (periodMs); // [5]
return success;
}
アプリケーションがシャットダウンしたら、以下の接続をキャンセルする必要がある。WebInputStream同時に実行されているものがある場合。最初に同じCriticalSectionオブジェクトにScopedLockを使用することで、以前に遭遇したコードのクリティカル・セクションが、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ドキュメントは、ボタンが1回押された場合、次のようになる:
// Root XML element for the whole document.
// Event node with name, type, timestamp and user ID.
// Parameters related to the parent event.
// Properties for the user in the parent event.
//...
を見ていく。saveUnloggedEvents()そしてrestoreUnloggedEvents()関数はそれぞれイベントの保存と復元を行う。そのsaveUnloggedEvents()関数は、上に示したフォーマットに基づいてXML構造を構築し、その内容をXMLファイルに保存します:
void saveUnloggedEvents (const std::deque& eventsToSave) override
{
// Save unsent events to disk. Here we use XML as a serialisation format, but
// you can use anything else as long as the restoreUnloggedEvents method can
// restore events from disk. If you're saving very large numbers of events then
// a binary format may be more suitable if it is faster - remember that this
// method is called on app shutdown so it needs to complete quickly!
juce::XmlDocument previouslySavedEvents (savedEventsFile);
std::unique_ptr 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& restoredEventQueue) override
{
juce::XmlDocument savedEvents (savedEventsFile);
std::unique_ptr 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 (xmlEvent->getIntAttribute ("timestamp")),
parameters,
xmlEvent->getStringAttribute ("user_id"),
userProperties
});
}
savedEventsFile.deleteFile(); // [7]
}
- [1]先ほどと同じように、あらかじめ定義されたファイルの場所に保存されているXMLファイルから、以前に保存されたイベントを取得しXmlElementそれをベースにしている。
- [2]もしXmlElementが存在しないか、ルート "events "ノードがない場合、何もすることがないので、関数から戻る。
- [3]まず、ルートの親ノードから解析するイベントの子ノードを1つ取得する。
- [4]子ノード "parameters "の各属性について、キーと値のペアを設定し、それをStringPairArray.
- [5]子ノード "user_properties "の各属性について、キーと値のペアを設定し、それをStringPairArray.
- [6]個々のイベントをイベント・キューに戻すには、対応するパラメータをStringPairArrayオブジェクトがある。
- [7]最後に、XMLファイルをディスクから削除する。
我々はXMLをシリアライゼーション・フォーマットとして使用したが、大量の未保存のイベントを保存する必要がある場合は、バイナリ・フォーマットの方が効率的だろう。
ログに記録されていないイベントを、以下のような別のシリアライズ形式で保存および復元する。JSONまたはバイナリ形式。
概要
このチュートリアルでは、GoogleアナリティクスとJUCEアナリティクスモジュールを使って利用データを追跡する方法を学びました。特に
- Google Analyticsにアナリティクスイベントを別スレッドで送信。
- 未送信のイベントをXMLドキュメントにローカルに保存。
- 保存されたイベントをXMLドキュメントからイベントキューに復元。