誰も知らないUsage Metrics(利用状況総計値)の話

# はじめに

Usage Metrics (利用状況総計値) という機能をご存知でしょうか？
管理パッケージとして配布したアプリケーションが
インストールされた組織でどのくらい利用されているのかを知ることができる、という
アプリケーションベンダーにとってはちょっと気になる機能です。

しかしながらこの機能、Summer'14 から正式リリースになったにも関わらず
いまだにWeb上には公式ドキュメント以外の記事がなかなか出てきません。
というわけで、今回はこの機能についてざっくり紹介してみようと思います。

# 有効化の手順

本機能はデフォルトでは有効化されていないので、Salesforce にケース申請して有効化してもらう必要があります。
その手順については以前こちらの記事に書きました。
[Usage Metrics（利用状況総計値）のインストール手順 - dackdive's blog](http://dackdive.hateblo.jp/entry/2015/02/24/211522)

ポイントとしては

- 環境ハブを有効化しておく必要がある
- 以下2つの組織が環境ハブで接続されている必要がある
  - リリース組織：パッケージのアップロードに使用する Development Edition 組織
  - レポート組織：利用状況データが毎日配信される組織

ということでしょうか。レポート組織は LMO でいいと思います。

# Usage Metrics で取得できる情報

管理パッケージをインストールした組織から取得できるのはカスタムオブジェクトとVisualforceページの情報です。

カスタムオブジェクトについてはレコード件数が、
VisualforceページについてはPV数やユニークユーザー数、平均読み込み時間が取得できます。
(詳しいデータ構造については後述)

# Usage Metrics データ収集のしくみ

Usage Metrics を有効化すると、レポート組織に毎日自動的にデータが配信されます。
データは `MetricsDataFile` というオブジェクト名で通常のSObjectと同じように扱うことができますが
SOQLやAPIでのアクセスのみ可能で、UIから参照することはできません。
また、特殊なデータだからか **データストレージは消費しません。**

`MetricsDataFile` のレコードはSalesforce のインスタンスごとに作成されます。
ので、1日に作成されるレコードは

```
2(カスタムオブジェクトとVisualforce) * Salesforce のインスタンス数
```

となります。
※ 複数のアプリケーションを開発している場合、さらに `* アプリケーション数`

# Usage Metrics のデータ型

Usage Metrics (というより `MetricsDataFile`) のデータ構造から主なものを抜き出してみます。
ref. https://developer.salesforce.com/docs/atlas.ja-jp.196.0.packagingGuide.meta/packagingGuide/usage_metrics_metricsdatafile_object.htm

| 項目名                     | 型       | 説明                                                         |
|----------------------------|----------|--------------------------------------------------------------|
| MetricsDataFile            | Base64   | **Base64で符号化された利用状況データが含まれるテキストファイル** |
| MetricsDataFileContentType | string   | データファイルの形式。<br />現在有効なのは text/csv のみ           |
| MetricsDataFileLength      | int      | データファイルのサイズ(バイト)                               |
| MetricsStartDate           | dateTime | データ収集の開始日時                                         |
| MetricsEndDate             | dateTime | データ収集の終了日時                                         |
| MetricsRunDate             | dateTime | データ収集ジョブが実行された日付                             |
| MetricsType                | picklist | データの種別<br />(`CustomObject` もしくは`Visualforce`)   |
| NamespacePrefix            | string   | データ収集の対象であるパッケージの<br />名前空間プレフィックス     |
| SendingInstance            | string   | このデータの収集元であるサーバインスタンス。                 |

`MetricsDataFile` というオブジェクトに `MetricsDataFile` というフィールドがあってややこしいですね。
また、Base64 で〜と書いてますが、少なくともApexの場合は `toString()` するだけでデータは取得できるのであまり気にしなくていいと思います。

```java
List<MetricsDataFile> mdfList = [
    SELECT
    	Id,
    	SendingInstance,
    	MetricsStartDate,
    	MetricsEndDate,
    	MetricsRunDate,
    	MetricsType,
    	MetricsDataFile
    FROM MetricsDataFile
    WHERE SendingInstance LIKE 'AP%'
    AND MetricsType = 'Visualforce'
    LIMIT 10];

for (MetricsDataFile mdf : mdfList) {
  String mdfDataCSV = mdf.MetricsDataFile.toString();
  System.debug(LoggingLevel.INFO, '-------------------');
  System.debug(LoggingLevel.INFO, mdf.SendingInstance);
  System.debug(LoggingLevel.INFO, mdf.MetricsStartDate);
  System.debug(LoggingLevel.INFO, mdf.MetricsEndDate);
  System.debug(LoggingLevel.INFO, mdf.MetricsRunDate);
  System.debug(LoggingLevel.INFO, mdf.MetricsType);
  System.debug(LoggingLevel.INFO, '**Data**');
  System.debug(LoggingLevel.INFO, mdfDataCSV);
}
```

`MetricsDataFile` というフィールドにお目当ての統計データがCSV形式で格納されるわけですが、
CSVのフォーマットはカスタムオブジェクトとVisualforceで異なります。

#### カスタムオブジェクトの場合

- 組織 ID
- 組織名
- 組織のエディション
- 組織のステータス (Active, Trial など)
- カスタムオブジェクト名
- カスタムオブジェクトのレコード件数

例）

```csv
"00Dxx0000001gbk","org1","Enterprise Edition","TRIAL","Alpha", "3500" 
"00Dxx0000001gbk","org1","Enterprise Edition","TRIAL","Beta", "1500"
```

org1 という組織では、ある日の時点で `Alpha` というカスタムオブジェクトが3500件、`Beta` というカスタムオブジェクトが1500件存在したことを示します。

#### Visualforceの場合

- 組織 ID
- 組織名
- 組織のエディション
- 組織のステータス
- パッケージバージョン番号
- Visualforce ページの名前
- ページがアクセスされた回数
- ページにアクセスしたユニークユーザ数
- ページの平均読み込み時間 (ミリ秒)

例)

```
"00Dxx0000001gbk","org1","Enterprise Edition","TRIAL","1.0","/apex/gm12__f1","65","11","353"
"00Dxx0000001gbk","org1","Enterprise Edition","TRIAL","1.0","/apex/gm12__f2","1","1","128.0"
"00Dxx0000001gbk","org1","Enterprise Edition","TRIAL","1.0","/apex/gm12__f3","1","1","107.0"
"00Dxx0000001gbf","org1","Enterprise Edition","TRIAL","1.0","/apex/gm12__f1","5","1","73.6"
"00Dxx0000001gbf","org1","Enterprise Edition","TRIAL","1.0","/apex/gm12__f2","1","1","72.0"
"00Dxx0000001gbf","org1","Enterprise Edition","TRIAL","1.0","/apex/gm12__f3","7","1","50.8"
```

org1 という組織ではある日の `gm12__f1` というページのPV数は65、ユニークユーザー数は11、平均読み込み回数は353msであったことを示します。

# 開発時のポイント

### まずは Usage Metrics Visualization から始めよう

どんなデータが見れるかいまいちイメージできない、何から始めたらいいかわかんないという人は
Usage Metrics Visualization というパッケージをインストールしてとりあえずデータを表示してみるのがいいと思います。
https://developer.salesforce.com/docs/atlas.ja-jp.196.0.packagingGuide.meta/packagingGuide/usage_metrics_visualization.htm

![196.png](https://qiita-image-store.s3.amazonaws.com/0/36518/0dccbba3-3e04-8d0d-8ac8-70b34f27c9eb.png)

Usage Metrics Visualization は AppExchange で配布されている未管理パッケージです。
https://appexchange.salesforce.com/listingDetail?listingId=a0N3000000B5FMvEAN

コードを読んだり独自でカスタマイズを加えることができるので
ここから徐々に自分の欲しいデータが見られるよう修正を加え、データの扱いに慣れてきたら自社に合った分析ツールを自作すると良いと思います。

### Sandbox でも`MetricsDataFile`は使えるが、データはコピーされない

`MetricsDataFile` がどの組織でも使えるものではない以上、開発はレポート組織のSandbox組織で行うことになると思います。
レポート組織で`MetricsDataFile`が有効になると、そのSandboxについてもメタデータにはアクセスできます。
ただ注意したいのが、**フルSandboxであっても`MetricsDataFile`のレコードはコピーされない** ということです。
(なんとかしてくれって [IdeaExchange](https://success.salesforce.com/ideaView?id=08730000000DlDEAA0) に投稿しました)

ですので、開発時は

- 本番組織から1週間分ぐらいの `MetricsDataFile` をダウンロードする
- 加工してテスト用のデータを作成し、Sandboxに静的リソースとしてアップロードする
- コード中の SOQL を実行している箇所でフラグに応じて静的リソースのデータを使うよう修正する

みたいなことが必要になると思います。

例として、Usage Metrics Visualization で実際にSOQLを実行しているのは `MDFQueryDAOImpl.cls` というクラスですが、
これを書き換えるとこんな感じでしょうか。

```java:MDFQueryDAOImpl.cls
public with sharing class MDFQueryDAOImpl implements MDFQueryDAO {

	private static final boolean IS_DEV = true;

	public List<MetricsDataFile> findMDFs(String sendingInstanceId, Date lookbackStartDate, Date lookbackEndDate) {
		if (IS_DEV) {
			return MDFMock.createMDFs(sendingInstanceId, lookbackStartDate, lookbackEndDate);
		} else {
			return [SELECT NamespacePrefix, MetricsDataFileLength, MetricsDataFile FROM MetricsDataFile
						WHERE SendingInstance = :sendingInstanceId
						AND MetricsStartDate >= :lookbackStartDate
						AND MetricsStartDate <= :lookbackEndDate];
		}
	}
	// (略)
```

```java:MDFMock.cls
public without sharing class MDFMock {

	public static List<MetricsDataFile> createMDFs(String sendingInstanceId, Date lookbackStartDate, Date lookbackEndDate) {
		List<MetricsDataFile> mdfList = new List<MetricsDataFIle>();
		// Custom Object
		List<StaticResource> srListCO = [SELECT Name, Body FROM StaticResource WHERE Name = 'MDFSampleCO' LIMIT 
1];
		// Visualforce
		List<StaticResource> srListVF = [SELECT Name, Body FROM StaticResource WHERE Name = 'MDFSampleVF' LIMIT 1];

		// --- 静的リソース -> MetricsDataFile への変換 ---

		return mdfList;
	}
```



### `with sharing` だと `MetricsDataFile` にデータをセットできない

これもなにげにハマりポイントです。
たとえば、以下のVisualforceは表示時にエラーになります。

```xml:MDFError.page
<apex:page controller="MDFController">
</apex:page>
```

```java:MDFController.cls
public with sharing class MDFController {

    public MDFController() {
        MetricsDataFile mdf = new MetricsDataFile();
        mdf.MetricsStartDate = Date.today();  // System.SObjectException: Field is not writeable
    }
}
```

正直 `MetricsDataFile` に関しては Daoクラスを `with sharing` にする意味があるのかもよくわかりませんが、
`MDFQueryDAOImpl.cls` を参考にするならば注意が必要です。

### テストクラスでも `MetricsDataFile` の insert は不可

`MetricsDataFile` は読み取り専用なのでレコードを insert しようとするとエラーになります。
これはテストクラスにおいても同様です。
さすがにこれもどうにかならんのかということで [IdeaExchange](https://success.salesforce.com/ideaView?id=08730000000DlHzAAK) に投稿してます。
(こっちの方は多少Voteがあってうれしい)

### ぶっちゃけ Usage Metrics Visualization の実運用には無理がある？

ここまで話しといてなんですが。
Usage Metrics Visualization がイケてないというより、Apex + Visualforce で実運用に耐える分析ツールを開発するのは難しいかも、という話です。

Usage Metrics Visualization を使ってみた印象ですが、**ガバナ制限エラーが頻発します。**
制限を超えてしまうのは CPU time であったりヒープサイズです。

参考までに、Winter'16 時点でのそれぞれの上限値は以下のようになっています。

| Description                            | Synchronous Limit                | Asynchronous Limit               |
|---------------------------------------------|---------------------|---------------------|
| Total heap size                            | 6 MB                | 12 MB               |
| Maximum CPU time on the Salesforce servers5 | 10,000 milliseconds | 60,000 milliseconds |

(https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm より引用)

それではどうすれば良いのかというと、`MetricsDataFile` はCSV部分だけ **日次で外部にエクスポートし、
分析ツールごと外部に構築してしまう** というのが現実的なんじゃないかなと思ってます。

幸い REST API からも `MetricsDataFile` にはアクセスできるので、REST API を定期的に叩いてデータをエクスポートするのがいいんではないでしょうか。
実はこれを私の好きなGoogle Apps Scriptで試してみようかなと思ったんですが今回は時間切れです。

Google Apps Scriptは[OAuth2認証のためのライブラリ](https://github.com/googlesamples/apps-script-oauth2)がありますしクーロンを登録してメソッドを定期的に実行することもできますのでオススメです。
前にGitHubとOAuth2で連携するというのを試したことがあるんですが、認証まわりは同じノリでいけないかなーと思ってます。
[[GoogleAppsScript]OAuth2でGitHubと連携し、issueをスプレッドシートに出力する - dackdive's blog](http://dackdive.hateblo.jp/entry/2015/11/30/192207)

# おわりに

というわけで、マイナーだけど気になる機能を知ってもらおうと思って書き始めたんですが
**いかに使いづらいか** を宣伝する結果になってしまった感があります。

ただ、冒頭でも書きましたが **`MetricsDataFile` は有効化してもデータストレージを消費しません** し、
紹介した Usage Metrics Visualization も **カスタムオブジェクトを含まない** ことを考えると
とりあえず入れてみてもいいのかなと思っています。


# リファレンス

ISVforce ガイド(日本語版は最新がSummer'15)
https://developer.salesforce.com/docs/atlas.ja-jp.196.0.packagingGuide.meta/packagingGuide/usage_metrics_intro.htm

Summer'14 リリースノート
http://releasenotes.docs.salesforce.com/ja-jp/summer14/release-notes/rn_forcecom_isvforce_usage_metrics.htm