Treasure DataをAndroidアプリで使う


AndroidアプリでTreasure Dataを組み込む事になったので色々調べた。

## Treasure Dataとは？

公式サイトURL
[Treasure Data - データ分析をクラウドで、シンプルに。 - Treasure Data - Treasure Data](https://www.treasuredata.co.jp/)

## 何が出来るのか？

コピペすると

```
データの収集・分析・連携を目的としたクラウド型データマネージメントサービスです。
ウェブ、モバイルアプリケーション、センサーの多構造化・非構造化データなど、
様々なソースからのデータ収集、分析、連携を簡単に行えるのが特徴です。
```

## どうやって組み込むのか？

ここから本番

以下の公式SDKリンクを読む（英語）
[Treasure Data Android SDK | Treasure Data](https://docs.treasuredata.com/articles/android-sdk)

もしくはGitHubのREADMEを読む（こっちがやや詳しい）
https://github.com/treasure-data/td-android-sdk#instantiate-treasuredata-object-with-your-api-key

ここからは、SDKリンク先とGitHubの説明を日本語化してみる。

といってもほぼGoogle翻訳からのコピペだけど。

## 前提
- Android 2.3以降
- API 14（Android 4.0）以降を実行しているAndroid搭載端末（ライフサイクルイベントの自動追跡用）
- トレジャーデータの基礎知識 （←！？）

５分の[説明動画](https://youtu.be/nVwTMABEouI)があるようだ。

## ステップ1：ライブラリをインストールする

app/build.gradleのdependenciesに追加

```gradle
dependencies {
  compile 'com.treasuredata:td-android-sdk:0.1.17'
}
```
2018/04/27時点では`0.1.17`が最新

## ステップ2：必要なAndroid権限を有効にする

通信するので`AndroidManifest.xml`に`android.permission.INTERNET`を追加する。

```xml
<uses-permission android:name="android.permission.INTERNET"/>
```

## ステップ3：TreasureDataオブジェクトをインスタンス化する

効率的なAPI呼び出しのために、カスタムApplicationクラスの`onCreate`メソッドでTreasureData共有インスタンスを初期化することを強くお勧めします。

```java
public class ExampleApp extends Application {

  @Override
  public void onCreate() {

    // Initialize Treasure Data Android SDK
    // @see https://docs.treasuredata.com/articles/android-sdk
    TreasureData.initializeApiEndpoint("https://in.treasuredata.com");
    TreasureData.initializeEncryptionKey("RANDOM_STRING_TO_ENCRYPT_DATA");
    TreasureData.disableLogging();
    TreasureData.initializeSharedInstance(this, "YOUR_WRITE_ONLY_API_KEY");
    TreasureData.sharedInstance.setDefaultDatabase("your_application_name");
    TreasureData.sharedInstance.setDefaultTable("your_event_name");
    TreasureData.sharedInstance.enableAutoAppendUniqId();
    TreasureData.sharedInstance.enableAutoAppendModelInformation();
    TreasureData.sharedInstance.enableAutoAppendAppInformation();
    TreasureData.sharedInstance.enableAutoAppendLocaleInformation();
  }
}
```

APIキーは、[Treasure Data Console](https://console.treasuredata.com/users/sign_in?redirect_to_path=%2Fapp%2Fusers%2Fcurrent&redirect_to_site=console)から取得できます。
SDKには**書き込み専用のAPIキー**を使用することをお勧めします。

次に、この`TreasureData.sharedInstance()`メソッドでどこからでも共有インスタンスを使用できます。

```java
public class ExampleActivity extends Activity {

  public void onDataLoadSomethingFinished(long elapsedTime) {
    Map<String, Object> event = new HashMap<String, Object>();
    event.put("data_type", "something");
    event.put("elapsed_time", elapsedTime);
    TreasureData.sharedInstance().addEvent("events", event);
  }
}
```

ライフサイクルイベント（アプリケーションオープン、インストール、アップデート）を自動的に追跡する機能は、デフォルトで有効になっています。
`disableTrackApplicationLifecycleEvents()`機能で無効にするか、個々のコアイベントを次のように無効にすることができます。

- アプリケーションのインストールを無効にする： `disableApplicationInstalledEvent()`
- アプリケーションを無効にする： `disableApplicationOpenEvent()`
- アプリケーションアップデートを無効にする： `disableApplicationUpdatedEvent()`

これらのイベントは、イベントの特定のタイプに応じて、関連するメタデータと共にキャプチャされます。

## ステップ4：識別する

`setUserId()`メソッドを使用すると、ユーザーをイベントに関連付けることができます。あなたのアプリケーションにユーザーを記録する独自のログインシステムがある場合、ユーザーのアカウントが最初に作成されるときに`setUserId`を一度呼び出すことをおすすめします。

```java
TreasureData.sharedInstance().setUserId("USER_ID");
```

各一意のユーザーIDがTD内の一意のユーザーとして解釈されるため、変更可能なユーザーIDをユーザーに割り当てないでください。IDはTD内に表示され、アプリケーション開発者はイベントのソースである正しいコンテキストでイベントを表示できます。

NOTE: 0.1.18以降ではGDPR対応により、setUserId関数はなくなり、addEventにて明示的にユーザIDを送る必要があります。


## ステップ5：イベントをローカルにバッファリングする

次に、`addEvent(String table, Map<String, Object> record)`アプリケーション内の適切なタイミングで関数を呼び出します。
次の例は`button_clicks`ボタンがクリックされたときにテーブルに送信されるイベントを示しています。

**アプリケーションの名前はデータベース名、イベント名はテーブル名として使用することをお勧めします。**

```java
View v = findViewById(R.id.button);
v.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    // シンプル版
    final Map event = new HashMap<String, Object>();
    event.put("name", "foo bar");
    event.put("age", 42);
    event.put("comment", "hello world");
    td.addEvent("button_clicks", event);

    // コールバックメソッドもある
   td.addEventWithCallback("testdb", "demotbl", event, new TDCallback() {
          @Override
          public void onSuccess() {
            Log.i("ExampleApp", "success!");
          }

          @Override
          public void onError(String errorCode, Exception e) {
            Log.w("ExampleApp", "errorCode: " + errorCode + ", detail: " + e.toString());
          }
        });
  }
});
```

この呼び出しでは、次のようにレコードがすぐにディスクにバッファリングされます。

```
{
  "td_uuid":"c123470de-3950-4997-b321-07d6f5b4a3",
  "td_os_type":"Android",
  "td_os_ver":"15",
  "td_device":"ISW11SC",
  "td_model":"ISW11SC",
  "td_display":"IMM76D.KDLPL",
  "td_brand":"KDDI",
  "td_board":"ISW11SC",
  "td_app_ver":"1.9.2",
  "td_app_ver_num":"11",
  "td_locale_country":"US",
  "td_locale_lang":"en",
  "name":"foo bar",
  "age":42,
  "comment":"helloworld"
}
```

デフォルトでは、すべてのイベントはディスクにバッファリングされます。バッファリングされたデータをクラウドに明示的にフラッシュする必要があります。アップロードは自動的には行われません。次のセクションでは、`uploadEvents()`関数を使用してバッファをフラッシュする方法について説明します。

## ステップ6：onStart() / onStop()でのセッショントラッキングとイベントアップロード

`onStart()`で`TreasureData.startSession()`を、`onStop()`で`TreasureData.endSession()`を呼び出します。
次に、`onStop()`で`uploadEvents()`を呼び出します。
`uploadEvents()`関数は、バッファをフラッシュし、データをクラウドに送信します。アップロードに失敗すると、その機能はバックグラウンドスレッドで自動的に再試行されます。

```java
protected void onStart(Bundle savedInstanceState) {
  TreasureData.startSession(this);
  td.addEvent("on_start_calls", ...); // optional but recommended
  Log.i(TAG, "onStart(): Session ID=" + TreasureData.getSessionId(this)); // You can get the current session ID like this
}

protected void onStop() {
  TreasureData.endSession(this);
  td.addEvent("on_stop_calls", ...); // optional but recommended
  td.uploadEvents();
}
```

アップロードするタイミングとバッファリングされたイベントをアップロードする頻度は、アプリケーションの特性によって異なります。アップロードには時間がかかります：

- 現在の画面が閉じている、または背景に移動しているとき
- アプリケーションを終了するとき

### アップロードと重複排除の再試行

このSDKは、次の機能を使用してイベントを1つの方法でインポートします。

- SDKは、追加された一意のキーでバッファされたイベントを保持し、イベントがアップロードされてサーバー側に（少なくとも1回）保存されるまでイベントをアップロードするように再試行します
- サーバー側は、既定で過去1時間以内のすべてのイベントの一意のキーを記憶し、重複したインポートを防止します（最大で1回）

重複排除ウィンドウはデフォルトで1時間ですので、重複したイベントを避けるために、バッファされたイベントを1時間以上保持しないことが重要です。

### セッションの開始/終了

`startSession()`を呼び出したが`endSession()`を呼び出していないContextがあれば、セッションは続行されます。
また、新しいContextが、最後のContextを呼び出してから10秒以内に`startSession()`を呼び出す場合、`endSession()`新しいセッションが作成されるのではなく、セッションが再開されます。

```java
	@Override
	protected void onStart(Bundle savedInstanceState) {
			:
		TreasureData.sharedInstance().startSession("demotbl");
			:
	}

	@Override
	protected void onStop() {
			:
		TreasureData.sharedInstance().endSession("demotbl");
		TreasureData.sharedInstance().uploadEvents();
		// Outputs =>>
		//   [{"td_session_id":"cad88260-67b4-0242-1329-2650772a66b1",
		//		"td_session_event":"start", "time":1418880000},
		//
		//    {"td_session_id":"cad88260-67b4-0242-1329-2650772a66b1",
		//		"td_session_event":"end", "time":1418880123}
		//    ]
			:
	}

```


注意
IPホワイトリストはAndroid SDKからのインポートには適用されません。また、Androidデバイスのタイムスタンプが無効であることがよくあります（たとえば、1970/01/01）。
このため、7日以上、3日以上経過したタイムスタンプを持つログは現在無視されます。

## エラーコード

エラーの原因を知りたい時、`TreasureData#addEventWithCallback`そして`TreasureData#uploadEventsWithCallback`コールバックの`TDCallback#onError`メソッドを`errorCode`引数があり、エラーの原因の種類を知るのに便利です。次のエラーコードがあります。

- init_error ：初期化に失敗しました。
- invalid_param ：APIに渡されたパラメータが無効です
- invalid_event ：イベントが無効です
- data_conversion ：JSONとのデータの変換に失敗しました
- storage_error ：ストレージ内のデータの読み書きに失敗しました
- network_error ：ネットワークの問題のためにサーバーとの通信に失敗しました
- server_response ：サーバーがエラー応答を返しました

## ヒント

### 配列型のデータ送信

Android SDKはjava.util.Listまたはjava.lang.String []を処理して、Array型のデータを送信できます。
しかし、内部ライブラリjackson.databindがJSONArrayを処理しないため、JSONArrayを処理しません。

```java
event.put("array", Arrays.asList("one", "two", "three"));
```

## SDKのGitHubリンク

- https://github.com/treasure-data/td-android-sdk
- https://github.com/treasure-data/td-android-sdk/tree/master/example
- [ChangeLog](https://github.com/treasure-data/td-android-sdk/blob/master/CHANGELOG.md)