iOS アプリの配布証明書を Cloud-managed certificates に移行した話

こんにちは。

iOS チームの池沢です。

最近、桃鉄( 2 ではなく定番の方 )でついに 100 年プレイをやってみました。

途中のデストロイ号1に何度も心を折られましたが、 68 年目になんとか全物件購入・全駅踏破を達成しました。 30 年くらい残ってますが、目標を失ってしまい結局最後まではプレイできていません。

桃鉄の話はさておき、今回は iOS アプリにおける証明書管理について書きます。

今までは年に 1 回証明書を更新する必要がありマニュアルを読み返してましたが、その作業から開放された喜びが伝わる記事になっていましたら幸いです。

背景

iOS アプリを App Store で公開したり社内に配布するためには、配布証明書と呼ばれるものを使ってアプリに署名する必要があります。

署名がされていないアプリは、 App Store Connect へアップロード出来ず、社内に配布しても iOS がそのアプリを信頼出来ないものと判定して開くことが出来ません。

課題

署名に使われる配布証明書ですが、生成されてから有効期限が 1 年しかありません。

そのため毎年 1 回、新しい配布証明書を生成する必要があります。

しかし、配布証明書の生成アプリは普段使わないため使い方を覚えられず、弊社内で前に作成されたマニュアルを必ず読み返す必要がありました。

そんな中、 Apple が 2021 年に Cloud-managed certificates というものをリリースしました。

これは証明書の発行や管理をすべてクラウドサービス側が行ってくれる2もので、上記の課題を解決するべく移行を行いました。

やったこと

App Store Connect で API キーを発行

App Store Connect で、 API キーを作成します。

ユーザーとアクセス > 統合 タブに移動し、+ ボタンからキーを作成出来ます。

role は Admin である必要があります。

作成したキーの他に、キー ID 、 Issuer ID を使うことになります。

ExportOptions.plist を作成

以下のファイルを作成します。私はプロジェクト直下に作成しました。

DESTINATION_METHOD , EXPORT_METHOD , APPLE_TEAM_ID は変数として用途ごとに設定しています。

DESTINATION_METHOD upload : ipa を作成し、 App Store Connect へアップロードする export : ipa を作成するのみ

EXPORT_METHOD app-store : App Store Connect へのアップロード用 adhoc : QA 用 enterprise : In-House 用 development : 開発用

APPLE_TEAM_ID Apple Developer Program を契約しているチーム ID

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>destination</key>
    <string>DESTINATION_METHOD</string>
    <key>manageAppVersionAndBuildNumber</key>
    <false/>
    <key>method</key>
    <string>EXPORT_METHOD</string>
    <key>signingStyle</key>
    <string>automatic</string>
    <key>stripSwiftSymbols</key>
    <true/>
    <key>teamID</key>
    <string>APPLE_TEAM_ID</string>
    <key>uploadSymbols</key>
    <true/>
</dict>
</plist>

署名無しで .xcarchive を作成

xcodebuild archive コマンドで .xcarchive を作成します。

.ipa を作成するにおいてこのタイミングでの署名は必須ではないため、 CODE_SIGNING_REQUIRED=NOCODE_SIGNING_ALLOWED=NO を指定して署名無しでアーカイブします。

$ xcodebuild archive \
    -workspace ${ .xcworkspace のパス } \
    -scheme ${ スキーム名 } \
    -configuration ${ Build Configuration 名 } \
    -archivePath ${ .xcarchive の生成先パス } \
    CODE_SIGNING_REQUIRED=NO \
    CODE_SIGNING_ALLOWED=NO \
    | xcpretty

Entitlements をビルドに埋め込む

署名なしで作成した .xcarchive には Entitlements 情報が含まれません。

そのため Push 通知や Sign in with Apple の機能を含むアプリの場合は、それらの機能が使えなくなってしまいます。

codesign コマンドを使って .xcarchive に Entitlements 情報を上書きすることで、 Push 通知や Sign in with Apple が使える .ipa に作成することが出来ます。

$ codesign \
    --entitlements ${ .entitlements のパス} \
    --force \
    --sign "-" ${ .xcarchive 内の .app のパス}

API キーを用いて .ipa を作成

xcodebuild -exportArchive コマンドで .ipa を作成します。最初の方で作成した API キーや ExportOptions.plist を使用します。

$ xcodebuild -exportArchive \
    -archivePath ${ .xcarchive のパス } \
    -exportPath ${ .ipa の生成先パス } \
    -exportOptionsPlist ${ ExportOptions.plist のパス } \
    -allowProvisioningUpdates \
    -authenticationKeyID ${ API キーのキー ID } \
    -authenticationKeyIssuerID ${ API キーの Issuer ID } \
    -authenticationKeyPath ${ API キーのパス } \
    | xcpretty

ipa が問題ないか確認

最後に作成した .ipa が問題ないかを確認します。

確認するためには、 .ipa の生成先パスに一緒に作成されている DistributionSummary.plist を確認します。

certificate が Cloud Managed Apple Distribution になっていて、 entitlements に aps-environmentcom.apple.developer.applesignin が含まれていれば問題ありません。

まとめ

iOS アプリの署名に使うための配布証明書を Cloud-managed certificates に移行する手順を記しました。

これで年に 1 回の作業時間がなくなったので、私はその時間で桃鉄の残り 30 年をプレイしたいと思います3

最後に

弊社では一緒にプロダクトを改善していただける仲間を探しています! こちらでお気軽にお声がけください! https://jmty.co.jp/recruit_top/


  1. 発生させた人だけではなく、プレイヤー全員が被害を受ける鬼畜列車。
  2. Apple Store Connect へアクセスすると最新の配布証明書が取得できる & 最新の配布証明書の有効期限が残り 90 日になると自動で新しい配布証明書を作ってくれる。
  3. 嘘です。業務時間にゲームは出来ないので、普通に iOS の実装をします。

Claude Codeで週次インフラレポートを自動化した話

ジモティーでバックエンドとインフラを担当している吉田です。

今月の三連休に北アルプスの立山に行ってきました。初日は室堂のターミナルから歩いてすぐの山小屋で一泊し、翌日は暴風雨で目を覚ましました。就寝前は多少の困難を覚悟していたのですが、あまりの激しさに撤退してきました。二泊三日の予定が、ほとんど交通機関の移動で終わり、とても残念でした。今年も残り数ヶ月、良いことが起こる気がしてなりません。

山小屋の長い夜が明けるとそこは嵐の中であった

今回は、インフラのステータスレポート作成と分析作業をClaude Codeによって自動化する取り組みを始めたので紹介します。

1. はじめに

ジモティーのインフラチームでは、毎週の定例会のなかで、サービスに関するインフラ観点での健全性を確認しています。

事前に複数のデータソースから情報を集約し、悪化を確認した場合はその原因について仮説を立て、対応の優先順位を決定する議論を行います。この作業は、リアルタイム監視では見逃しがちな中長期的なパフォーマンス劣化の傾向を発見するための重要な位置づけになっています。

しかし、準備での詳細な調査も含めると1時間以上もかかることがあり、メンバーの負担となっていました。

2. 実現したもの

複数のデータソースから情報を統合し、包括的なレポートを自動生成します。レポートには異常を検知したサービスメトリクスの概要、タイムライン、影響範囲、そして原因仮説と対応案などが含まれています。

次は自動生成したレポートの例です。

インフラレポートの例

3. 自動化の仕組みとアーキテクチャ

Claude Codeを選んだ理由

週次レポート作成は、複数のデータソース(Mackerel、BigQuery)を横断的に分析し、異常パターンの相関を見つけて原因仮説を立てる複雑なタスクです。これまでは各データソースを個別に目視確認し、データソース間の関連性を調べる必要がありました。

Claude Codeなら、複数のデータソースを自動的に横断分析し、「先週のスパイクも含めて分析して」といった自然言語での柔軟な指示が可能です。探索的な調査も自律的に実行できます。また、日常的な開発で使い慣れていたため効率的にカスタマイズできました。

さらに、ジモティーのエンジニアは会社からの補助により全員がMaxプランに加入済みのため、追加費用なく利用できる点も決め手となりました。

スラッシュコマンドによるかんたん実行

Claude Codeには、よく使う処理をスラッシュコマンドとして登録できる機能があります。これが今回の用途に合っていたため、 /infra-report というカスタムスラッシュコマンドを作成しました。

/infra-report

Claude Codeのプロンプトでこの一行を入力すると、データ収集→分析→レポート生成→品質チェックまでの工程が自動実行されます。

処理の流れ

処理全体は以下の4ステップで自動実行されます。

  1. データ収集(並列処理)

    • Mackerelメトリクス:ロードバランサーのレイテンシーやリクエスト数、MongoDBの各種メトリクスなどをmkrコマンドで収集
    • BigQueryログ:アプリケーションで取得したアクセスログをもとにレスポンスタイムのパーセンタイル分析、エンドポイント別統計、時間帯別トラフィックなどをBigQueryからMCPで収集
  2. 統合分析

    • スパイク検出(200ms閾値超過の自動検知)
    • 異常パターンの相関分析
    • 前週・前月比での変化傾向把握
  3. レポート生成

    • Claude Codeによる仮説立案
    • 優先度付き改善提案の作成
    • Markdownフォーマットでの構造化レポート作成
  4. 品質検証

    • 生成内容の妥当性チェック
    • 問題があれば自動的に再調査を実行
    • 最終レポートの完成

以下の図は、各エージェントがどのように連携して動作するかを示しています。

コマンドのフロー

各ステップに対応した4つのサブエージェントが連携して処理を実行します。

各サブエージェントは独立したコンテキストを持つため、専門領域に特化した処理が可能です。

また、データ収集フェーズでは並列実行により処理時間を50%以上短縮しました。品質検証で問題が見つかった場合、自動的に再調査を行います。

ジモティー専用コンテキストリポジトリの活用

もともとBacklogチケットの状態遷移判断や開発作業の生成AI支援のためにjmty_contextリポジトリが存在します。Claude Codeが社内システムを深く理解し、様々な開発・運用タスクを支援できるように設計されています。

これをインフラレポート生成にも活用することにしました。リポジトリのレポート生成に関する構成は次の通りです。

jmty_context/
├── CLAUDE.md                  # システム全体の概要
├── CLAUDE-architecture.md     # アーキテクチャ詳細
├── CLAUDE-repositories.md     # 各リポジトリの説明
├── .claude/
│   ├── agents/
│   │  ├── metrics-collector   # Mackerelメトリクス収集エージェント
│   │  ├── logs-collector      # アクセスログ収集エージェント
│   │  ├── report-generator    # レポート作成エージェント
│   │  └── report-reviewer     # レポート品質検証エージェント
│   └── commands/
│       └── infra-report.md    # スラッシュコマンド
└── templates/
    └── infra-report.md        # レポートテンプレート

4. 技術的な課題と解決アプローチ

課題1: 原因仮説精度

Claude Codeが誤った原因仮説を生成してしまう問題がありました。実際のバッチ処理スケジュール、過去の傾向データ、エンドポイント別の特性といったコンテキスト情報を充実させることで対応しています。

課題2: タイムゾーン変換

BigQueryとMackerelでは日付や日時情報をUTCで管理していますが、レポートでは日本時間(JST)で確認する必要がありました。これらのタイムスタンプをJSTに変換する処理を実装し、ハードコードではなく「先週のメトリクス」といった相対的な時間表現で柔軟性を確保しています。

課題3: レポート形式

Claude Codeは毎回異なるフォーマットで出力するため、以下のような項目を定義したMarkdownテンプレートを用意しました。

  • 要約
  • パフォーマンスメトリクス
  • エラー分析
  • 異常条件と原因分析
  • 調査で実行したSQL

5. 導入による変化と今後の課題

これまで実施していた作業の自動化により、特にデータ収集の工数が短縮できました(数十分→数分)。

課題としては、パフォーマンス悪化時の原因分析において以下の情報源が不足していることで、改善の必要があると感じています。

  • MongoDBのクエリログ: 適切でないインデックスが選択されることでスロークエリが発生するケースがあり、この詳細な分析情報が必要
  • Elasticsearchのクエリログ: 検索クエリのパフォーマンス問題を特定するため、スロークエリログの収集と分析が必要
  • アプリケーションレイヤーのログ: DBなどインフラレイヤーではなく、アプリケーション自体のコードやメモリリークなどが原因となるパフォーマンス劣化の柔軟な検出

これらの情報は現在、自動レポートで検出した異常に対して、エンジニアが必要に応じて手動で調査・補足している状況です。例えば、MongoDBのクエリプランの詳細確認や、特定エンドポイントのコードレビューなどを個別に実施し、レポートの分析結果を補強しています。

今後は上記のデータソースも統合することで、インフラとアプリケーションの両面から問題を特定し、さらに精度の高い分析が可能になると考えています。

6. まとめ

週次インフラレポートの作成および分析の自動化により、定例業務の効率化と分析品質の向上を同時に実現する取り組みについてご紹介しました。

生成AIの台頭によって、「これができたら開発業務やチーム運営が楽になるのにな」と思っていたことが、以前よりも簡単に実現できるケースが増えてきました。これまでの経験や知恵を生成AIの膨大な知識で補強することによって、問題解決における新しいアプローチの探求につながっていると思います。改めてソフトウェアエンジニアリングの楽しさを実感しています。

7. さいごに

ジモティーではエンジニアを積極的に採用しています。

ご興味のある方は採用情報をご覧ください。

「その他」カテゴリの投稿を各カテゴリに再分類するインターン課題について

はじめに

北見工業大学大学院にて自然言語処理の研究を行っております、重延(シゲノブ)と申します。

全国で140名程度しかいない珍しい苗字ですので、覚えていただければ幸いです。

2024年8月より株式会社ジモティーにて、インターン生としてエンジニアリング業務に従事させていただいております。

今回はサマーインターンでの自身の取り組みについて、テックブログとしてまとめたいと思います。

今回のサマーインターンでは研究室のメンバー3名で参加させていただきました。

インターンの目標として与えられたのは、各部署の課題を自分たちの研究で得た知見を活かして解決するということでした。

まず初めに課題のヒアリングを行い、その結果として以下のようなテーマが挙がりました。

  • その他カテゴリに埋もれている投稿の再分類
  • 距離に応じて商品を提案する仕組み作り
  • 問い合わせ数の予測
  • 取引完了に至らない理由の分析
  • 類似投稿やNG投稿の自動チェック
  • 外部連絡先を交換していないかの自動チェック

この中で私は「その他カテゴリに埋もれている投稿の再分類」という課題に着目し、その解決をサマーインターンの目標と定めました。

背景と目的

ユーザーが売買取引を行うにあたって商品カテゴリを付与する際に、以下のような課題が指摘されていました。

  • 適切なカテゴリが見つけられない
  • カテゴリ選択が手間に感じられる

その結果、投稿がその他カテゴリに分類されるケースが多く発生していました。

これまでにも画像からカテゴリを推定してサジェストする仕組みなど、いくつかの対策が取られてきました。

しかし、それでもその他カテゴリの投稿割合に大きな変化は見られなかったとのことです。

事前調査として2024年8月の全売買投稿件数(689,047件)のカテゴリ毎の割合を示します。

このようにその他カテゴリが最多となっており、割合にして19.9%となっております。

次にその他カテゴリにどれほど誤分類されているかを調査します。

2023年8月以降のその他カテゴリ投稿からランダムに400件抽出し、私の判断によってカテゴリを再度振り分けた結果を以下に示します。

  1. 「その他」カテゴリに分類された投稿のうち、76.7%が別のカテゴリに振り分け可能だった。
  2. 全投稿におけるカテゴリ割合と類似しており、「その他」カテゴリが正しく機能していないことを示唆している。

これは全投稿の割合から考えると月ごとに約10万件がその他カテゴリに埋もれてしまい、見つけられにくくなっている状況であると言えます。

これにより検索精度の低下や成約機会の喪失、データ分析精度の低下といった問題が生じる恐れがあります。

そこでその他カテゴリをあらためてチェックし、テキストベースでのカテゴリの再分類ができないかを考察します。

手法

このようなマルチラベル分類における手法は、大別すると以下が考えられます。

  • 辞書ベース分類
    • 特定のカテゴリに対応する単語をあらかじめ定義、それに基づき分類を行う。
  • 従来型の機械学習モデルによる分類
    • SVMやソフトマックス回帰などの機械学習アルゴリズムを用いて、特徴量に基づき分類を行う。
  • 大規模言語モデル(LLM)を使用した分類
    • BERTなどの事前学習済みモデルを用いて、ゼロショット分類やファインチューニングによる分類を行う。

今回その他カテゴリのデータを確認すると固有名詞が多く、辞書ベースの分類は困難であると感じられました。

またそれぞれ商品名などは簡潔に書かれており、商品の特徴は強く現れると考えられます。

しかし使用感やサイズ、状態などの周辺情報も利用できると考え、文脈の理解が得意なTransformerモデルを使用した分類を行うことに決定しました。

はじめにゼロショット分類を行いどの程度分類が可能かを調査し、その上でファインチューニングを行い比較します。

実験結果

評価指標として以下の5つを使用します。

・正解率(Accuracy):正、負と予測したデータが正解している割合

・Top3-Accuracy:予測した上位3つまでが正解している割合

・適合率(Precision):正と予測したデータが、実際に正である割合

・再現率(Recall):実際に正であるデータが、正と予測された割合

・F値(F-measure):再現率と適合率の調和平均

教師なし学習

  • 使用モデル:MoritzLaurer/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7
    • マルチリンガルに対応したNLI(自然言語推論)に特化したモデルの一つであり、マルチラベルのゼロショット分類に広く用いられているため使用
  • データセット:事前アノテーションによってその他から別カテゴリに振り分けられた307件
  • ラベル:その他以外の19カテゴリ

ゼロショット分類結果

使用データ Acc Top-3 Acc Prec Rec F1
タイトルのみ 0.494 0.713 0.522 0.597 0.448
タイトル+テキスト 0.492 0.736 0.513 0.602 0.482

結果として全文を使用した場合の方が高スコアとなり、そのF1スコアは0.482とあまり高い数値とはなりませんでした。

エラー分析をすると、以下の例のように複数要素が混じった商品においてミスが多く見られました。

よってファインチューニングを行い、カテゴリへの理解を深める他、タイトルへの重み付けなどの対策が考えられます。

Title Text 予測ラベル 正解ラベル
ピカチュウのポーチ カバンの中でパイナップルジュースを爆発させてしまったときにカバンに入っていたものなので、ニオイと汚れがかなりあります。... 食品 靴/バッグ

教師あり学習

次に、ファインチューニングを行った分類結果を示します。

  • 使用モデル:tohoku-nlp/bert-base-japanese-whole-word-masking
    • 日本語に特化し、Whole Word Maskingを活用した事前学習済みモデルであり、文脈理解に優れているもの
  • データセット:44697件
    • その他以外の19カテゴリが均等になるようにランダム抽出し、データ整形をした上で利用
  • ラベル:その他以外の19カテゴリ

ファインチューニング後の分類結果

学習率 Acc Top-3 Acc Prec Rec F1
1e-5 0.779 0.889 0.784 0.779 0.781
2e-5 0.781 0.899 0.789 0.784 0.785

F1スコアは最高で0.785とかなり大きな数値の向上が見られました。

これは投稿の傾向を理解し、カテゴリ間の関係性を深く理解できるようになった結果であると考えられます。

また、今回はタイトルの重み付けやパラメータの微調整が不十分であるため、更なる精度向上が期待できます。

まとめ

その他カテゴリに誤分類された投稿を、約8割弱の確率で分類することができました。

これにより、課題解決の一歩を踏み出す成果を得られたと考えられます。

今後はカテゴリの修正方法を工夫するとともに、ファインチューニングの手法そのものを改善することでより高精度な分類が可能になると考えられます。

今後は私自身がこのモデルの実装を担当する運びとなりました。

実際の開発環境でモデルを運用する貴重な経験を活かし、さらなるスキル向上に努めていきたいと思います。

ジモティーにおけるエンジニアへの問い合わせの継続的な取り組み

ジモティーでバックエンドとインフラを担当している吉田です。

およそ10年ぶりに登山用のバックパックを新調したので、さっそく神奈川県の丹沢に行ってきました。鍋割山から塔ノ岳、三ノ塔と縦走して、いったんヤビツ峠に降りたあと、大山まで行くのがお気に入りのコースです。丹沢には何年も通っていますが、今年始めてヒルに吸血されました(一度に両足全3箇所)。

丹沢大山の山頂付近にて

さて、今回は、社内のエンジニアが継続的に行っている、エンジニアへの問い合わせの取り組みについて紹介します。

エンジニアへの問い合わせとは?

まず、「エンジニアへの問い合わせ」についての説明です。これは、非エンジニアメンバー(カスタマーサービスやコーポレート部門など)からエンジニアへ日常的に行われる質問や相談のことを指します。

例えば次のような問い合わせがあります。

  • 🙋‍♀️ 「データ分析ツールがエラーになるので対応をお願いします」
  • 🙋 「連携サービスでメンテナンスが予定されているので、対応が必要か確認をお願いします」
  • 🙋‍♂️ 「この機能改修の工数見積もりをお願いします」

これらの問い合わせは、すぐに解決できるものから、目的や背景を確認して代替案を相談しながら解決に向かうものまで様々です*1

⛰️1合目: 特定エンジニアへの集中

私が入社した2017年4月頃、エンジニアと非エンジニアに関わらず、問い合わせ先は特定のベテランエンジニアに集中していました。

そのエンジニアは在籍年数が長く、サービス仕様の経緯や組織の内情に非常に詳しいため、「エンジニアリングの質問や問題はあの人に聞けばすぐに解決できる」という認識が広がっていたと思います。

この問題は次になります。

  1. 質問者と対応者の2者間で情報が閉じているので、同じ質問が発生する
  2. 対応する特定のエンジニアに対応負荷が集中する

実際、そのエンジニアの座席に問い合わせ者の列ができる事もありました。

一度に一人しか通れない橋

⛰️3合目: 問い合わせ対応のオープン化

そこで、2017年5月に問い合わせ専用の Slack チャンネルを開設しました。

チャンネルは「エンジニア外との会話」と「エンジニア内での会話」の2つで、用途によって使い分けるようにしました。

  1. エンジニアへの質問部屋
    • 用途: 他部署からエンジニアへ質問、相談用
    • エンジニア以外のメンバーに対しても分かりやすい説明が必要
  2. エンジニア間の質問部屋
    • 用途: エンジニア同士で質問、相談用
    • より専門的な議論が可能

この施策によって情報がオープンになり、複数のエンジニアが回答するようになったため、問題が一気に改善しました。開設当時のメッセージを読み返すと、問題解決や議論の好きなエンジニアが多く、副次的にエンジニア内外のメンバー間の交流も進んだと思います。

また、問い合わせの対応ログや知見は情報共有ツールの Kibela に残す事を推奨し、特定のメンバーしか対応できない問い合わせをなるべく少なくする方針も周知されました。

⛰️5合目: エンジニアリングサポートチームの発足

数年間は大きな問題もなく運用できていたのですが、徐々に放置されたり、最初の反応に時間のかかる問い合わせが多くなっていきます。そして、ついに他部署からたびたび改善要望が発生するようになってしまいました。

この要因は次であったと分析しています。

  1. メンバーが入れ替わり、エンジニア内で対応の相談が気軽にできなくなっていた
    • そのため、DM でやりとりされている事も多かった
  2. 担当案件に集中するあまり、優先順位がいつの間にか落ちていた

そこで、2023年1月に問い合わせ対応専門のエンジニアリングサポートチームを立ち上げました。

バックエンド担当の若手メンバーが中心で、気軽に相談し合えるように専用の Slack チャンネルを開設しました。また、3ヶ月ごとに設定する個人目標の一部にこの取り組みの成果を含め、メンバーの OKR を統一する事で、チームとして取り組めるようにしました。

⛰️8合目: 輪番制の導入と目的の確認

しばらくすると、今度は対応するメンバーの固定化が問題になってきました。

対応したメンバーはその経験によって、知識の幅が広がり、さらに回答できるようになります。そのメンバーにとっては非常に良い成長サイクルが出来ているのですが、チームや組織全体で捉えるとそのメンバーに強く依存する事になってしまい、望ましくないと考えました。

そこで、問い合わせがあるとエンジニアリングサポートチームのメンバーに順番に自動でアサインする仕組みを導入しました*2。ただし、アサインされていないメンバーが取り組むことや、アサインされたメンバーでも手が離せない場合は申告してスキップもOKとしています。

アサイン通知の例

また、取り組みの目的を「事業を継続する最低限のサービス品質維持」と定義しました。サービスを成長させる上で間接的ではあるものの、一定必要であると考えています。

そして、一次回答までの時間をエンジニアグループで担保するサービス品質の1つとして KPI に組み込んでモニタリングしています。

これらの施策によって、対応時間が短縮され、放置される問い合わせはほとんどなくなりました。

週次の問い合わせ件数と一次回答までの平均時間

まとめ

ジモティーにおける、他部署からエンジニアへの問い合わせの問題とその解消に向けた試行錯誤の歴史を振り返りました。

今後もまた問題が出てくる可能性は十分にあるため、隔週でミーティングを設定して、もし何か相談事項があれば開催しています。

現在を山で例えると8合目だとしても、数年後に振り返ると実はまだ1合目だったのかもしれません。いずれにしても、発生する問題に対して継続して改善を続け、事業を円滑に進める基盤であり続けたいと考えています。

丹沢三ノ塔付近から烏尾山、行者ヶ岳を望む

最後に

ジモティーではエンジニアを積極的に採用しています。

ご興味のある方は採用情報をご覧ください。

*1:ヒアリングの結果、そもそも解決すべき問題ではない事もあります。

*2:Slack の webhook を Sinatra のアプリで受け取って、チームメンバーに順番にメンションするアプリを実装しました。

Flutter 製アプリで Android ・ iOS のライブラリを使用する

こんにちは、iOS チームの池沢と申します。

最近「鎌倉殿の 13 人」を見返していますが、やはり最高の大河ドラマだと思います。

上総広常の最後のシーンは、今後も何度も見返すんだろうなと思います。

大河ドラマの話はさておき、今回は私が先日まで行っていた Flutter での機能開発の話を書こうと思います。

他のエンジニアからも時々聞かれる話ですので、イメージしやすい話になっていれば幸いです。

背景

ジモティーではゴミの削減やリユースの促進を目的に、自治体と連携しながらリアル店舗(ジモティースポット)の運営を行っております。

先日この取り組みをさらに加速させていくため、初の大型店舗を神奈川県川崎市にオープンしました。

ジモティーでは一般に公開しているアプリの他に、この店舗内で使われる業務用アプリの開発も行っております。

開発は Flutter で行っており、リユース品の管理やジモティーへの投稿など業務効率化のためのさまざまな機能があります。

課題

背景で説明したように、業務効率化のため業務用アプリにもさまざまな機能開発を行っています。

その一つとして、先日ジモティースポットにとあるシステム(例えば、コンビニやスーパーにあるセルフレジのようなものをイメージしていただけたらと思います)を導入しようという動きがありました。

しかし、そのシステムをアプリから使えるようにするために調査していたところ、 Flutter 用のライブラリは用意されておらず Android ・ iOS それぞれにライブラリが用意されておりました。

実装

Flutter には Android ・ iOS のそれぞれのネイティブコードを呼び出すための仕組みがあります。

今回はそれを用いることで、ライブラリが Flutter 向けでなくてもそのライブラリを使えるようにしました。

Method Channel とは

Flutter は、プラットフォーム固有の API をそれらと連携する言語で呼び出すことができます。

その仕組みの中で最も有名だと思うのが Method Channel と呼ばれる方法です。

以下で、この Method Channel を用いた具体的な実装方法を、 Android を例にとって記します。

Flutter 側

はじめに、チャンネルを作成します。

final platform = MethodChannel('jmty.flutter.dev/samples');

その後、 invokeMethod によりメソッドを呼び出します。ジェネリクスの型は、返り値の型を指定します(指定しないと dynamic 型になります)。

final result = await platform.invokeMethod<String>('cash_register')

以上で呼び出しは完了です。

Android 側で呼び出したメソッドの処理が完了すると、その返り値が result に入ります。

Android 側

Flutter プロジェクトには android フォルダがあり、その中に Android 用アプリのコードが格納されています。

その中の MainActivity.kt は FlutterActivity を継承しており configureFlutterEngine メソッドが定義されていますが、 MethodChannel を使用するとこの中に処理が入ります。

class MainActivity: FlutterActivity() {
  private val CHANNEL = "jmty.flutter.dev/samples"

  override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      call, result ->
      // 下で追記
    }
  }
}

この中で、呼んだメソッド毎に処理を実装します。

正常終了すれば success とともに返り値を渡し、正常終了できなければ error と共にエラーコードやエラーメッセージを渡します。

メソッドが定義されてなければ、 notImplemented を呼びます。

MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
  call, result ->
  when (call.method) {
    "cash_register" -> {
      val methodResult = sampleMethod() // 処理の実装
      if (methodResult) {
        result.success("success") // 正常終了
      } else {
        result.error("failure", "error message", null) // 異常終了
      }
    }
    else -> {
      result.notImplemented()
    }
  }
}

これで実装は終了です。

正常終了した場合、 dart の result には success が入ります。

異常終了した場合は PlatformException が throw されます。

まとめ

Flutter で Android ・ iOS のライブラリを使う手順を記しました。

Flutter はやはり開発体験が良く柔軟性も高いので、開発していて楽しいですね。

KMP も stable 版がリリースされ、今後はよりクロスプラットフォーム開発が広まっていくことを期待しています。

最後に

ジモティーでは、一緒に課題を解決してくれるエンジニアを募集中です!大きな成長を遂げたいという強い想いをお持ちの方にジョインしていただけることを待ち望んでおります! https://jmty.co.jp/recruit_top/

ジモティーAdsフロントエンド構成

はじめに

フロントエンドとバックエンドを担当している川崎です。

今回は前回に引き続き、ジモティーAdsについてお話しします。

広告主様向けの広告出稿や効果レポートの確認が行える管理画面のフロントエンド構成についてご紹介します。

ジモティーAdsとは

ジモティーに直接出稿できる運用型の広告配信プラットフォームです。

「ジモティーAds」で出稿された広告をジモティーのスマートフォン向けアプリ・webブラウザのフィード面に配信することができます。

ジモティーに登録されたユーザー情報や過去の行動履歴から、ユーザーにとって最適な広告を配信できることが特徴です。

ジモティー本体の構成と比較

前提として、どちらもNext.jsのバージョン13以降を採用しています。

ジモティー ジモティーAds
ルーター PageRouter AppRouter
静的生成 (Static Export) なし あり
レンダリング SSR、SSG CSR
スタイリング styled-componentsで自作 MUI

特徴

ルーター

時流的に推奨されているAppRouterが長期的な保守を考えた場合に有効だと考え、採用しています。

しかし、後述するスタイリングや静的生成の関係で、その恩恵を十分に受けているわけではありません。

数少ない有用な機能としては、Nested Layoutがあります。

layoutファイルをアクターやログイン状態ごとに継承させることで、一定の単位で共通レイアウトを定義できます。

ログイン判定なども、直下のlayout内で行うことで、ページごとに記述する必要がありません。

app
├── layout.tsx
├── (admin) - 管理者
│   ├── layout.tsx
│   ├── (authenticated) - ログイン要
│   │   ├── layout.tsx
|   |   ├── not-found.tsx
│   │   ├── error.tsx
|   |   └── hoge
|   |       └── page.tsx
│   └──(unauthenticated) - ログイン非
│      ├── layout.tsx
|      ├── not-found.tsx
|      └── hoge
|          └── page.tsx
└── (adsmaneger) - 広告主
   ├── layout.tsx
   ├── (authenticated) - ログイン要
   │   ├── layout.tsx
   |   ├── not-found.tsx
   |   └── hoge
   |       └── page.tsx
   └──(unauthenticated) - ログイン非
       ├── layout.tsx
       ├── not-found.tsx
       └── hoge
           └── page.tsx

静的生成(Static Export)

アプリケーションを静的コンテンツとしてエクスポートできる機能です。

Node.jsサーバーを構築する必要がなく、コスト面で大きな利点があります。

使用できない機能がいくつかあり、動的ルーティングが使えない点に特に注意が必要でした。

generateStaticParamsでIDを割り振る方法はビルド時に解決されてしまうため、実質的な動的ルーティングの解決にはなりません。

そこで、クエリパラメータにてIDを指定し、必要なリソースを取得することで解決しました。

Nginxなどで動的なパスのリクエストを都度振り分けるよりも、インフラ側の負担を軽減できる利点があります。

× ad/edit/[:id]
○ ad/edit?id=[:id]

レンダリング

静的生成を利用すると、レンダリング方法がCSRに強制されます。

管理画面であり、SEO要件を気にする必要がないので採用に踏み切れました。

SWRを通してキャッシュなどのパフォーマンス最適化を図っています。

サーバーコンポーネントでのフェッチも調査しましたが、以下の問題があり採用を見送りました。

  • MUIがサーバーコンポーネントに対応していない
  • 静的生成を利用すると(≒SSRを利用すると)ビルド時にしかフェッチされない

スタイリング

本体側は旧Railsのスタイルを引き継ぐ必要があり、UIフレームワークを導入すると逆にスタイルの定義が大変でした。

弊社独自のデザインシステムに沿って自前でスタイリングを行っています。

一方で、今回は新規プロダクトかつ管理画面で高度なデザインが必要ないため、MUIの恩恵を存分に受けています。

その他主要ライブラリ

  • SWR
  • react-hook-form
  • zod
  • openapi-fetch
  • storybook
  • msw

まとめ

Next.jsはサービスごとの特性に応じて幅広い構成を選択できるのが利点です。

バージョン13以降、大幅なアップデートが続いており、追従するのは大変ですね。。

理解できる範囲で取捨選択し、有効活用していきたいです。

Andrioidアプリ広告におけるメディエーション機能について

はじめに

ジモティーでAndroidとiOSの開発をしている3年目の坂本です。

最近はマユリカというお笑いコンビのマユリカのうなげろりん!!というラジオを聴くのにハマっています。

特に#36の回、めちゃくちゃ笑いました。ポッドキャストで聴けるので気になった方はぜひ!

さて、今回はジモティーAdsについて書きたいと思います。

ジモティーAdsについて

2023年11月にジモティーAdsをリリースしました🎉

ジモティー独自のデータを活用することで、より高い広告効果の提供を目指しています。

ジモティーアプリで今まで表示していたAdMob広告とジモティーAdsを共存させるために、AdMobのメディエーション機能を採用しています。

メディエーションについて

メディエーションについて、以下のように紹介されています。

メディエーションは、アプリでの広告配信に使用する広告ソースを 1 か所で管理できる機能です。
メディエーションを使用すると、届いた広告リクエストを複数の広告ソースに送信し、使用可能かつ最適な広告ソースを確実に見つけて広告を掲載できます。

つまり、AdMobにリクエストを投げたらいろんな広告ソースの中から最適な広告を返してくれるので、広告掲載率と収益を向上が期待できるというわけです。

その最適な広告ソースの選ばれ方は以下の2種類あります。

  • 入札
  • ウォーターフォール

入札

入札は、リクエストを受け取るとリアルタイムで入札オークションが行われ、見事オークションを落札した広告が選ばれるみたいです。

ウォーターフォール

ウォーターフォールは、リクエストを受けると全ての広告のeCPMが再計算され、最上位のネットワークから順に紹介が行われ、リクエストに一致する広告があれば配信されます。

以下の例だと、eCPMが4ドルのネットワークDが選ばれ、リクエストに一致した広告があればネットワークDが配信されます。

一致する広告がなければ、次に高い3ドルのAdMobネットワークの広告が配信されます。

メディエーションにジモティーAdsを組み込む

今回はその中にジモティーAdsを組み込みます。

ジモティーAds単体だと3.5$ですが、メディエーション機能を使うことでネットワークDの4$の広告を表示できるようになります。

https://support.google.com/admob/answer/13420272?hl=ja

実装

カスタムイベントを作成

まずはカスタムイベントを管理画面で作成します。(詳しくはこちらJmtyCustomEventなどカスタムイベントアダプタを実装するクラスの名前を決めます。

そのクラスの完全修飾名を管理画面のClass Nameに設定します。 → 例: com.google.ads.mediation.sample.customevent.HogeHogeCustomEvent

アダプターを初期化する

Google Mobile Ads SDK が初期化されると、アプリ用に設定されたすべてのサードパーティ製アダプタとカスタムイベントに対してinitialize()が呼び出されるそうです。

今回作成したJmtyCustomEventでもinitialize()メソッドが走るので、初期化完了のメソッドを呼び出します。

class JmtyCustomEvent : Adapter() {

    override fun initialize(
        context: Context,
        initializationCompleteCallback: InitializationCompleteCallback,
        mediationConfigurations: MutableList<MediationConfiguration>,
    ) {
        Log.d(TAG, "initialize: JmtyAdsMediation")
        initializationCompleteCallback.onInitializationSucceeded()
    }
}

バージョン番号の報告

カスタムイベントを作成したら、カスタムイベントアダプタ自体のバージョンと、サードパーティSDKのバージョンの両方をGoogle Mobile Ads SDKに報告する必要があります。

今回ジモティーAdsの部分はSDK化していないため1.0.0にしています。

具体的な実装は省略しますが、気になる方は以下のドキュメントに実装コードがありますのでご確認ください。 https://developers.google.com/admob/android/custom-events/setup#report_version_numbers

リクエスト

今まで通りロードメソッドを呼ぶ

リクエストの仕方は今まで通りのloadAdメソッドを呼ぶだけです。

        adView.loadAd(request)

例えば以下のようなウォーターフォールの順番の場合、ネットワークDの広告があればそれがAdViewとして表示され、JmtyCustomEventは起動しません。

ネットワークDがない場合、ジモティーAdsが選ばれJmtyCustomEventloadBannerAdメソッドが呼び出されます。

class JmtyCustomEvent : Adapter() {

    private lateinit var bannerLoader: JmtyCustomEventLoader

    override fun initialize(
        context: Context,
        initializationCompleteCallback: InitializationCompleteCallback,
        mediationConfigurations: MutableList<MediationConfiguration>,
    ) {
        Log.d(TAG, "initialize: JmtyAdsMediation")
        initializationCompleteCallback.onInitializationSucceeded()
    }

    override fun loadBannerAd(
        adConfiguration: MediationBannerAdConfiguration,
        callback: MediationAdLoadCallback<MediationBannerAd, MediationBannerAdCallback>,
    ) {
        bannerLoader = JmtyCustomEventLoader(adConfiguration, callback)
        bannerLoader.loadAd()
    }
}

JmtyCustomEventLoaderでは、実際にジモティーAdsのサーバーに広告のリクエストを行います。

class JmtyCustomEventLoader(
    private val mediationBannerAdConfiguration: MediationBannerAdConfiguration,
    private val mediationAdLoadCallback: MediationAdLoadCallback<MediationBannerAd, MediationBannerAdCallback>,
) : JmtyAdListener, MediationBannerAd {

    private lateinit var adView: JmtyAdView

    fun loadAd() {
         // ジモティーAdsのリクエスト
         // Viewの生成
    }

    override fun getView(): View {
        return adView.getView()
    }
}

リクエスト時のパラメータ

ジモティーAdsのリクエスト時にパラメータを渡したいが、リクエスト時にはJmtyCustomEventのアダプターに直接渡すことができません。

この解決策としてAdRequest.addNetworkExtrasBundleを使います。

リクエストする箇所で、Bundleに値を詰めてアダプタークラスを指定してAdRequestに渡します。

        val jmtyAdRequestBundle = Bundle().apply {
                putInt("id", 1)
        }
        val request = AdRequest.Builder()
                .addNetworkExtrasBundle(
                    JmtyCustomEvent::class.java,
                    jmtyAdRequestBundle,
                )
                .build()
        adView.loadAd(request)

指定したJmtyCustomEventのアダプターのMediationBannerAdConfiguration.mediationExtrasから取り出すことができます。

class JmtyCustomEventLoader(
    private val mediationBannerAdConfiguration: MediationBannerAdConfiguration,
    private val mediationAdLoadCallback: MediationAdLoadCallback<MediationBannerAd, MediationBannerAdCallback>,
) : JmtyAdListener, MediationBannerAd {
    fun loadAd() {
        val id = mediationBannerAdConfiguration.mediationExtras.getInt("id", -1)
    }
}

ジモティーAdsがない場合

mediationAdLoadCallback.onFailureを呼び出します。

class JmtyCustomEventLoader(
    private val mediationBannerAdConfiguration: MediationBannerAdConfiguration,
    private val mediationAdLoadCallback: MediationAdLoadCallback<MediationBannerAd, MediationBannerAdCallback>,
) : JmtyAdListener, MediationBannerAd {

    private lateinit var adView: JmtyAdView

    fun loadAd() {
         // ジモティーAdsのリクエスト

        // 広告がない時
        val adError = AdError(JmtyAdErrorCode.NO_FILL.errorCode, code.getErrorMessage(), ERROR_DOMAIN)
        mediationAdLoadCallback.onFailure(adError)
        return
    }
}

ジモティーAdsがなかったことが通知され、次のネットワークへリクエストが移り、AdMobネットワークがあればその広告がAdViewで表示されます。

Viewの生成

サーバーから受け取った広告の情報を使ってViewを作成しています。

作成したViewはMediationBannerAdgetView()メソッドに渡します。これでAdViewでジモティーAdsを表示できるようになります。

class JmtyCustomEventLoader(
    private val mediationBannerAdConfiguration: MediationBannerAdConfiguration,
    private val mediationAdLoadCallback: MediationAdLoadCallback<MediationBannerAd, MediationBannerAdCallback>,
) : JmtyAdListener, MediationBannerAd {

    private lateinit var adView: JmtyAdView

    fun loadAd() {
         // ジモティーAdsのリクエスト
         // Viewの生成
    }

    override fun getView(): View {
        return adView.getView()
    }
}

最後に

今回はリリースしたジモティーAdsをAdMobメディエーションに組み込んだ経緯やその方法について紹介させていただきました。

今後はジモティーAdsのCPMをあげていって配信比率を増やしていけるように、改善していければなと思います。


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです