ACS UI ライブラリでのユーザー フィードバックの収集

イントロダクション

この包括的なガイドは、開発者がバックエンド処理に Azure サービスを使用して ACS UI ライブラリに拡張サポートを統合するのを支援するように設計されています。 このガイドは、実装をわかりやすくするために、クライアント側とサーバー側の手順に分かれています。

[前提条件]

  • Azure サブスクリプション: アクティブな Azure サブスクリプションが必要です。 お持ちでない場合は、 Azure 無料アカウントで無料アカウントを作成できます。
  • Azure Communication Services リソース: ACS リソースは、通話機能とチャット機能を使用するために必要です。 Azure portal で作成できます。
  • 開発環境のセットアップ: 1 つ以上のターゲット プラットフォーム (Android、iOS、または Web) 用に開発環境が設定されていることを確認します。
  • Azure ストレージ アカウント: ユーザー フィードバックと関連データを安全に格納するには、Azure Storage アカウントが必要です。
  • Node.js と Express.js: Node.js と Express.js に関する基本的な知識は、サポート要求を受信して処理するようにサーバー側アプリケーションを設定する場合に役立ちます。
  • RESTful API の知識: RESTful API を作成して使用する方法を理解することは、クライアントとサーバーの両方の展開と構成に役立ちます。
  • クライアント開発スキル: Android または iOS アプリケーションの開発能力。

これらの前提条件を満たすと、Azure Communication Services やその他の Azure リソースを使用した包括的なユーザー フィードバック システムの統合がスムーズに開始されます。

学習内容

このガイドでは、Azure Communication Services (ACS) アプリケーション内のユーザー フィードバック メカニズムの統合に関する包括的な分析情報を取得します。 ACS UI ライブラリを通じてカスタマー サポートを強化し、Azure バックエンド サービスを使用して処理することに重点を置きます。 このガイドに従うことで、開発者は次の学習を行います。

  • Client-Side フィードバック キャプチャを実装します。 ACS UI ライブラリを使用して、Android および iOS アプリケーションから直接ユーザー フィードバック、エラー ログ、およびサポート要求をキャプチャする方法について説明します。
  • Server-Side アプリケーションを設定します。 Express.js を使用して Node.js アプリケーションを設定して、サポート要求を受信、処理、および Azure Blob Storage に格納する手順について説明します。 このサーバーには、ファイルアップロード用のマルチパート/フォームデータの処理と、ユーザーデータの安全な管理が含まれます。
  • サポート チケットの作成: 一意のサポート チケット番号を生成し、関連するアプリケーション データと共にユーザー フィードバックを格納する方法について説明します。
  • Azure Blob Storage を利用する: Azure Blob Storage を使用してフィードバックとサポート要求データを格納し、効率的な取得と分析をサポートする安全で構造化されたデータ管理を確保する方法について説明します。
  • アプリケーションの信頼性とユーザー満足度の向上: 開発者は、このガイドに記載されている戦略を実装することで、ユーザーの問題にすばやく対処して解決できます。

Server-Side 設定

サポート要求を処理するための Node.js アプリケーションの設定

セクションの目的: 目標は、ユーザーからのサポート要求を受け取るバックエンドとして機能する Express.js を使用して、Node.js アプリケーションを作成することです。 これらの要求には、テキスト フィードバック、エラー ログ、スクリーンショット、およびユーザーの問題の診断と解決に役立つその他の関連情報が含まれる場合があります。 アプリケーションは、整理された安全なアクセスのために、このデータを Azure Blob Storage に格納します。

フレームワークとツール

  • Express.js: Web アプリケーションと API を構築するための Node.js フレームワーク。 これは、サーバーのセットアップと要求処理の基礎として機能します。
  • Formidable: フォームデータを解析するためのライブラリで、特にmultipart/form-dataの処理を目的に設計されています。これはファイルのアップロードによく使用されます。
  • Azure Blob Storage: 大量の非構造化データを格納するための Microsoft Azure サービス。

手順 1: 環境のセットアップ

開始する前に、Node.js がインストールされた開発環境の準備ができていることを確認します。 また、送信されたデータを格納するには、Azure Storage アカウントへのアクセスも必要です。

  1. Node.jsをインストールします。 Node.js がシステムにインストールされていることを確認します。 Node.jsからダウンロードできます。

  2. Azure Blob Storage アカウントを作成します。 まだ作成していない場合は、Azure portal を使用して Azure Storage アカウントを作成します。 このアカウントは、サポート要求データを格納するために使用されます。

  3. 必要な資格情報の収集: Azure Blob Storage アカウントの接続文字列があることを確認します。

手順 2: アプリケーションのセットアップ

  1. 新しい Node.js プロジェクトを初期化します。

    • プロジェクトの新しいディレクトリを作成し、 npm init で初期化して package.json ファイルを作成します。

    • npm を使用して、Express.js、Formidable、Azure Storage Blob SDK、およびその他の必要なライブラリをインストールします。

      npm install express formidable @azure/storage-blob uuid
      
  2. サーバーの実装:

    • Express.js を使用して、特定のエンドポイントで POST 要求をリッスンする基本的な Web サーバーを設定します。
    • Formidable を使用して、受信フォーム データを解析し、マルチパート/フォーム データ コンテンツを処理します。
    • サポート要求ごとに一意のチケット番号を生成します。これを使用して、Azure Blob Storage 内のデータを整理し、ユーザーに参照を提供できます。
    • ユーザー メッセージやログ ファイルメタデータなどの構造化データを BLOB ストレージ内の JSON ファイルに格納します。 実際のログ ファイルとスクリーンショットまたは添付ファイルを、同じチケットのディレクトリ内の別の BLOB に格納します。
    • サポートの詳細を取得するためのエンドポイントを指定します。これには、Azure Blob Storage からのデータのフェッチと表示が含まれます。
  3. セキュリティに関する考慮事項:

    • アプリケーションで受信データが検証され、悪意のあるペイロードから保護されていることを確認します。
    • 環境変数を使用して、Azure Storage 接続文字列などの機密情報を安全に格納します。

手順 3: アプリケーションの実行とテスト

  1. 環境変数:

    • Azure Blob Storage 接続文字列とその他の機密情報の環境変数を設定します。 たとえば、 .env ファイル (およびこれらの変数を読み込むには dotenv npm パッケージ) を使用できます。
  2. サーバーの実行:

    • node <filename>.jsを実行して Node.js アプリケーションを起動します。ここで、<filename>はメイン サーバー ファイルの名前です。
    • Web 開発に適したツールを使用してサーバーを検証します。

サーバー コード:

ここでは、最初に動作する実装を示します。 このコードは、ACS UI サンプル アプリケーションからのチケット作成を示すために調整された基本的な実装です。

const express = require('express');
const formidable = require('formidable');
const fs = require('fs').promises
const { BlobServiceClient } = require('@azure/storage-blob');
const { v4: uuidv4 } = require('uuid');
const app = express();
const connectionString = process.env.SupportTicketStorageConnectionString
const port = process.env.PORT || 3000;
const portPostfix = (!process.env.PORT || port === 3000 || port === 80 || port === 443) ? '' : `:${port}`;

app.use(express.json());

app.all('/receiveEvent', async (req, res) => {
    try {
        const form = new formidable.IncomingForm();
        form.parse(req, async (err, fields, files) => {
            if (err) {
                return res.status(500).send("Error processing request: " + err.message);
            }
            // Generate a unique ticket number
            const ticketNumber = uuidv4();
            const blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
            const containerClient = blobServiceClient.getContainerClient('supporttickets');
            await containerClient.createIfNotExists();

            // Prepare and upload support data
            const supportData = {
                userMessage: fields.user_message,
                uiVersion: fields.ui_version,
                sdkVersion: fields.sdk_version,
                callHistory: fields.call_history
            };
            const supportDataBlobClient = containerClient.getBlockBlobClient(`${ticketNumber}/supportdata.json`);
            await supportDataBlobClient.upload(JSON.stringify(supportData), Buffer.byteLength(JSON.stringify(supportData)));

            // Upload log files
            Object.values(files).forEach(async (fileOrFiles) => {
                // Check if the fileOrFiles is an array (multiple files) or a single file object
                const fileList = Array.isArray(fileOrFiles) ? fileOrFiles : [fileOrFiles];
            
                for (let file of fileList) {
                    const blobClient = containerClient.getBlockBlobClient(`${ticketNumber}/logs/${file.originalFilename}`);
                    
                    // Read the file content into a buffer
                    const fileContent = await fs.readFile(file.filepath);
                    
                    // Now upload the buffer
                    await blobClient.uploadData(fileContent); // Upload the buffer instead of the file path
                }
            });
            // Return the ticket URL
            const endpointUrl = `${req.protocol}://${req.headers.host}${portPostfix}/ticketDetails?id=${ticketNumber}`;
            res.send(endpointUrl);
        });
    } catch (err) {
        res.status(500).send("Error processing request: " + err.message);
    }
});

// ticketDetails endpoint to serve details page
app.get('/ticketDetails', async (req, res) => {
    const ticketNumber = req.query.id;
    if (!ticketNumber) {
        return res.status(400).send("Ticket number is required");
    }

    // Fetch the support data JSON blob to display its contents
    try {
        const blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
        const containerClient = blobServiceClient.getContainerClient('supporttickets');
        const blobClient = containerClient.getBlobClient(`${ticketNumber}/supportdata.json`);
        const downloadBlockBlobResponse = await blobClient.download(0);
        const downloadedContent = (await streamToBuffer(downloadBlockBlobResponse.readableStreamBody)).toString();
        const supportData = JSON.parse(downloadedContent);

        // Generate links for log files
        let logFileLinks = `<h3>Log Files:</h3>`;
        const listBlobs = containerClient.listBlobsFlat({ prefix: `${ticketNumber}/logs/` });
        for await (const blob of listBlobs) {
            logFileLinks += `<a href="/getLogFile?id=${ticketNumber}&file=${encodeURIComponent(blob.name.split('/')[2])}">${blob.name.split('/')[2]}</a><br>`;
        }

        // Send a simple HTML page with support data and links to log files
        res.send(`
            <h1>Ticket Details</h1>
            <p><strong>User Message:</strong> ${supportData.userMessage}</p>
            <p><strong>UI Version:</strong> ${supportData.uiVersion}</p>
            <p><strong>SDK Version:</strong> ${supportData.sdkVersion}</p>
            <p><strong>Call History:</strong> </p> <pre>${supportData.callHistory}</pre>
            ${logFileLinks}
        `);
    } catch (err) {
        res.status(500).send("Error fetching ticket details: " + err.message);
    }
});

// getLogFile endpoint to allow downloading of log files
app.get('/getLogFile', async (req, res) => {
    const { id: ticketNumber, file } = req.query;
    if (!ticketNumber || !file) {
        return res.status(400).send("Ticket number and file name are required");
    }

    try {
        const blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
        const containerClient = blobServiceClient.getContainerClient('supporttickets');
        const blobClient = containerClient.getBlobClient(`${ticketNumber}/logs/${file}`);

        // Stream the blob to the response
        const downloadBlockBlobResponse = await blobClient.download(0);
        res.setHeader('Content-Type', 'application/octet-stream');
        res.setHeader('Content-Disposition', `attachment; filename=${file}`);
        downloadBlockBlobResponse.readableStreamBody.pipe(res);
    } catch (err) {
        res.status(500).send("Error downloading file: " + err.message);
    }
});

// Helper function to stream blob content to a buffer
async function streamToBuffer(stream) {
    const chunks = [];
    return new Promise((resolve, reject) => {
        stream.on('data', (chunk) => chunks.push(chunk));
        stream.on('end', () => resolve(Buffer.concat(chunks)));
        stream.on('error', reject);
    });
}


app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

クライアント側のセットアップ

このセクションでは、クライアント側のセットアップと、次の目標を達成する方法について説明します。

  1. ユーザーから報告された問題に登録します。
  2. データをシリアル化します。
  3. サーバーに転送します。
  4. 応答を受信します。
  5. ユーザーに応答を表示します。

Azure Communication Services (ACS) UI ライブラリ内でユーザー フィードバックを有効にするには、開発者側でのアクションが必要です。 ライブラリの統合に onUserReportedIssueEventHandler を利用することで、開発者は組み込みのサポート フォームを有効にして、ユーザーが問題を直接報告できるようになります。 このセクションでは、クライアント側のフィードバック フォームを設定する手順について説明します。

Android での Client-Side フィードバック キャプチャの実装

サポート フォームの有効化

  1. イベント ハンドラーの登録:

    • Android アプリケーション内でサポート フォームをアクティブにするには、アプリケーションのライフサイクルの適切な時点で onUserReportedIssueEventHandler を登録します。 この登録により、フォームが有効になるだけでなく、フォームが表示され、ユーザーがアクセスできるようになります。
  2. フォームの可視性とアクセシビリティ:

    • 登録された onUserReportedIssueEventHandler が存在すると、サポート フォームの可視性に直接影響します。 このハンドラーがないと、フォームはユーザー インターフェイスから非表示のままであり、問題報告のためにアクセスできなくなります。

サポート イベントのキャプチャと処理

  1. 問題報告時のイベントの発生:

    • 有効なサポート フォームを使用してユーザーが問題を報告すると、 onUserReportedIssueEventHandler は生成されたイベントをキャプチャします。 これらのイベントは、説明、エラー ログ、潜在的なスクリーンショットなど、ユーザーから報告された問題に関連するすべての必要な詳細をカプセル化します。
  2. 送信のためのデータ準備:

    • ユーザーが問題を報告したら、次の手順では、報告された問題データをサーバー送信用に準備します。 この準備には、キャプチャされた情報を HTTP 転送に適した形式に構成し、サーバーの期待に従って構成することが含まれます。

問題データをサーバーに送信する

  1. 非同期データ転送:

    • 非同期メカニズムを使用して、準備されたデータを指定されたサーバー エンドポイントに送信します。 この方法により、アプリケーションの応答性が維持され、バックグラウンドでデータが送信されている間にスムーズなユーザー エクスペリエンスが提供されます。
  2. サーバー応答の処理:

    • データの送信時には、サーバーの応答を正確に処理することが重要です。 この処理には、サーバーフィードバックを解析してデータ転送が成功したことを確認し、ユーザーに伝えることができる送信された問題 (チケット番号や URL など) への参照を抽出することが含まれる場合があります。

ユーザーフィードバックと通知の提供

  1. 即時のユーザー フィードバック:

    • アプリケーションのユーザー インターフェイスを使用して、問題レポートの送信の状態をすぐにユーザーに通知します。 送信が成功した場合は、ユーザーがレポートの進行状況を追跡できる、送信された問題への参照を指定することを検討してください。
  2. Android O 以降の通知戦略:

    • Android O (API レベル 26) 以降を実行しているデバイスの場合は、レポートの送信に固有の通知チャネルの実装を確認します。 このセットアップは、通知を効果的に配信するために不可欠であり、これらの Android バージョンでは必須です。

これらの手順に従うことで、開発者は、効率的な問題の報告と追跡のために onUserReportedIssueEventHandler を使用して、堅牢なユーザー フィードバック メカニズムを Android アプリケーションに統合できます。 このプロセスは、ユーザーの問題をタイムリーに解決できるだけでなく、アプリケーションに対する全体的なユーザー エクスペリエンスと満足度の向上にも大きく貢献します。

Android コード サンプル

Kotlin コード スニペットは、Azure Communication Services を使用して Android アプリケーション内でユーザーから報告された問題を処理するためのシステムを統合するプロセスを示しています。 この統合は、ユーザーとサポート チーム間の直接的なコミュニケーションを可能にすることで、サポート プロセスを効率化することを目的としています。 関連する手順の概要を次に示します。

  1. イベント キャプチャ: システムは、ACS UI ライブラリを介してユーザーから報告された問題をリッスンします。 onUserReportedIssueEventHandlerを利用して、エラーやユーザーの懸念事項など、アプリケーションの UI からのフィードバックをキャプチャします。

  2. サーバーへのデータ転送: 問題が報告されると、ユーザー メッセージ、エラー ログ、バージョン、診断情報など、関連するデータがパッケージ化されます。 その後、このデータは非同期 POST 要求を使用してサーバー エンドポイントに送信され、プロセスによってアプリのパフォーマンスが妨げられることはありません。

  3. ユーザーのフィードバックと通知: 送信後、ユーザーはアプリ内通知を通じて、レポートの状態についてすぐに通知されます。 送信が成功した場合、通知には、送信されたチケットへのリンクまたは参照が含まれるため、ユーザーは解決の進行状況を追跡できます。

このセットアップは、ユーザーの問題に迅速に対処するのに役立つだけでなく、サポートとフィードバックのための明確なチャネルを提供することで、ユーザーの満足度とアプリの信頼性を高めることにも大きく貢献します。

package com.azure.android.communication.ui.callingcompositedemoapp

import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.azure.android.communication.ui.calling.CallCompositeEventHandler
import com.azure.android.communication.ui.calling.models.CallCompositeCallHistoryRecord
import com.azure.android.communication.ui.calling.models.CallCompositeUserReportedIssueEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.asRequestBody
import org.threeten.bp.format.DateTimeFormatter
import java.io.File
import java.io.IOException

/**
 * This class is responsible for handling user-reported issues within the Azure Communication Services Calling UI composite.
 * It implements the CallCompositeEventHandler interface to listen for CallCompositeUserReportedIssueEvents.
 * The class demonstrates how to send diagnostic information to a server endpoint for support purposes and
 * how to provide user feedback through notifications.
 */
class UserReportedIssueHandler : CallCompositeEventHandler<CallCompositeUserReportedIssueEvent> {
    // Flow to observe user reported issues.
    val userIssuesFlow = MutableStateFlow<CallCompositeUserReportedIssueEvent?>(null)

    // Reference to the application context, used to display notifications.
    lateinit var context: Application

    // Lazy initialization of the NotificationManagerCompat for managing notifications.
    private val notificationManager by lazy { NotificationManagerCompat.from(context) }

    /**
     * Handles the event when a user reports an issue.
     * - Creates a notification channel for Android O and above.
     * - Updates the userIssuesFlow with the new event data.
     * - Sends the event data including user message, app and SDK versions, call history, and log files to a server.
     */
    override fun handle(eventData: CallCompositeUserReportedIssueEvent?) {
        createNotificationChannel()
        userIssuesFlow.value = eventData
        eventData?.apply {
            sendToServer(
                userMessage,
                debugInfo.versions.azureCallingUILibrary,
                debugInfo.versions.azureCallingLibrary,
                debugInfo.callHistoryRecords,
                debugInfo.logFiles
            )
        }
    }

    /**
     * Prepares and sends a POST request to a server with the user-reported issue data.
     * Constructs a multipart request body containing the user message, app versions, call history, and log files.
     */
    private fun sendToServer(
        userMessage: String?,
        callingUIVersion: String?,
        callingSDKVersion: String?,
        callHistoryRecords: List<CallCompositeCallHistoryRecord>,
        logFiles: List<File>
    ) {
        if (SERVER_URL.isBlank()) { // Check if the server URL is configured.
            return
        }
        showProgressNotification()
        CoroutineScope(Dispatchers.IO).launch {
            val client = OkHttpClient()
            val requestBody = MultipartBody.Builder().setType(MultipartBody.FORM).apply {
                userMessage?.let { addFormDataPart("user_message", it) }
                callingUIVersion?.let { addFormDataPart("ui_version", it) }
                callingSDKVersion?.let { addFormDataPart("sdk_version", it) }
                addFormDataPart(
                    "call_history",
                    callHistoryRecords.map { "\n\n${it.callStartedOn.format(DateTimeFormatter.BASIC_ISO_DATE)}\n${it.callIds.joinToString("\n")}" }
                        .joinToString("\n"))
                logFiles.filter { it.length() > 0 }.forEach { file ->
                    val mediaType = "application/octet-stream".toMediaTypeOrNull()
                    addFormDataPart("log_files", file.name, file.asRequestBody(mediaType))
                }
            }.build()

            val request = Request.Builder()
                .url("$SERVER_URL/receiveEvent")
                .post(requestBody)
                .build()

            client.newCall(request).enqueue(object : Callback {
                override fun onFailure(call: Call, e: IOException) {
                    CoroutineScope(Dispatchers.Main).launch {
                        onTicketFailed(e.message ?: "Unknown error")
                    }
                }

                override fun onResponse(call: Call, response: Response) {
                    CoroutineScope(Dispatchers.Main).launch {
                        if (response.isSuccessful) {
                            onTicketCreated(response.body?.string() ?: "No URL provided")
                        } else {
                            onTicketFailed("Server error: ${response.message}")
                        }
                    }
                }
            })
        }
    }

    /**
     * Displays a notification indicating that the issue ticket has been created successfully.
     * The notification includes a URL to view the ticket status, provided by the server response.
     */
    private fun onTicketCreated(url: String) {
        showCompletionNotification(url)
    }

    /**
     * Displays a notification indicating that the submission of the issue ticket failed.
     * The notification includes the error reason.
     */
    private fun onTicketFailed(error: String) {
        showErrorNotification(error)
    }

    companion object {
        // The server URL to which the user-reported issues will be sent. Must be configured.
        private const val SERVER_URL = "${INSERT_YOUR_SERVER_ENDPOINT_HERE}"
    }

    /**
     * Creates a notification channel for Android O and above.
     * This is necessary to display notifications on these versions of Android.
     */
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name = "Report Submission"
            val descriptionText = "Notifications for report submission status"
            val importance = NotificationManager.IMPORTANCE_DEFAULT
            val channel = NotificationChannel("report_submission_channel", name, importance).apply {
                description = descriptionText
            }
            notificationManager.createNotificationChannel(channel)
        }
    }

    /**
     * Shows a notification indicating that the report submission is in progress.
     * This uses an indeterminate progress indicator to signify ongoing activity.
     */
    private fun showProgressNotification() {
        val notification = NotificationCompat.Builder(context, "report_submission_channel")
            .setContentTitle("Submitting Report")
            .setContentText("Your report is being submitted...")
            .setSmallIcon(R.drawable.image_monkey) // Replace with an appropriate icon for your app
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setProgress(0, 0, true) // Indeterminate progress
            .build()

        notificationManager.notify(1, notification)
    }

    /**
     * Shows a notification indicating that the report has been successfully submitted.
     * The notification includes an action to view the report status via a provided URL.
     */
    private fun showCompletionNotification(url: String) {
        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
        val pendingIntent = PendingIntent.getActivity(
            context,
            0,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )

        val notification = NotificationCompat.Builder(context, "report_submission_channel")
            .setContentTitle("Report Submitted")
            .setContentText("Tap to view")
            .setSmallIcon(R.drawable.image_monkey) // Replace with an appropriate icon for your app
            .setContentIntent(pendingIntent)
            .setAutoCancel(true) // Removes notification after tap
            .build()

        notificationManager.notify(1, notification)
    }

    /**
     * Shows a notification indicating an error in submitting the report.
     * The notification includes the reason for the submission failure.
     */
    private fun showErrorNotification(error: String) {
        val notification = NotificationCompat.Builder(context, "report_submission_channel")
            .setContentTitle("Submission Error")
            .setContentText("Error submitting report\nReason: $error")
            .setSmallIcon(R.drawable.image_monkey) // Replace with an appropriate icon for your app
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .build()

        notificationManager.notify(1, notification)
    }
}

イベントのハンドラーを作成したら、呼び出し複合を作成するときに登録できます。

            callComposite.addOnUserReportedEventHandler(userReportedIssueEventHandler)

iOS サポートの概要

Azure Communication Services (ACS) UI ライブラリを使用して iOS アプリケーション内にユーザー フィードバック収集を統合するには、開発者は構造化されたアプローチに従う必要があります。 このプロセスには、エラー ログやユーザー情報など、ユーザーフィードバックのキャプチャが含まれます。 完了すると、この情報は処理のためにサーバーに送信されます。 このセクションでは、このタスクを実行するために必要な手順について詳しく説明します。

この例では、 アラモファイア ライブラリを使用して、ログ ファイルを含むマルチパート フォームをサーバーに送信する処理を行っています。

サポート フォームの実装

  1. イベント ハンドラーの登録: まず、ユーザーから報告された問題をリッスンするイベント ハンドラーを登録します。 このハンドラーは、ACS UI ライブラリの機能を使用して、iOS アプリケーションのインターフェイスから直接フィードバックをキャプチャするために重要です。

  2. フォームの可視性とアクセシビリティ: サポート フォームに簡単にアクセスでき、アプリケーション内のユーザーに表示されるようにします。 フォームのアクティブ化は、UI 内での外観をトリガーするイベント ハンドラーの実装に直接リンクされ、ユーザーは問題を報告できます。

サポート要求のキャプチャと処理

  1. ユーザー アクションでのイベントの発生: ユーザーがサポート フォームを介して問題を報告すると、イベント ハンドラーはこのアクションをキャプチャします。 問題に関するユーザーの説明、エラー ログ、呼び出し ID などの情報は、サーバーに送信できるように準備する必要があります。

  2. 送信のためのデータ構造化: キャプチャされた情報を送信に適した構造化された形式に整理します。 サポート要求を受信して処理するサーバー エンドポイントの想定される形式に合わせてデータを準備します。

サーバーへのデータの送信

  1. 非同期送信: 非同期ネットワーク呼び出しを使用して、構造化データをサーバーに送信します。 この方法により、アプリケーションの応答性が維持され、バックグラウンドでデータが送信される間にユーザーにシームレスなエクスペリエンスが提供されます。

  2. サーバー応答の処理: 送信時に、サーバーの応答を効率的に処理します。 応答を受信して解析し、データが正常に受信されたことを確認します。 解析された応答からサポート チケット リンクを抽出します。このリンクは、フォローアップのためにユーザーに伝えることができます。

ユーザーへのフィードバックと通知

  1. 即時受信確認: アプリケーション内でのサポート要求の送信を直ちに確認し、レポートが受信されたことをユーザーに確認します。

  2. 通知戦略: 特に特定の通知フレームワークをサポートする iOS バージョンを実行しているデバイスで、ユーザーに通知を配信するための戦略を実装します。 ローカル通知を使用して、ユーザーにレポートの状態を通知したり、問題に対処するときに更新を提供したりできます。

iOS コード サンプル

この Swift コード サンプルでは、ユーザーから報告された問題をキャプチャし、処理のためにサーバーに送信するための基本的な実装の概要を示します。 この例では、ユーザー フィードバック、アプリケーション診断情報、サーバーへの配信など、サポート イベント ハンドラーを構築する方法を示します。 このコードには、スムーズなユーザー エクスペリエンスを確保するためのエラー処理とユーザー通知戦略も含まれています。

次の例は、イベント ハンドラー内にインストールするフックとして設計されています。

Installation

let onUserReportedIssueHandler: (CallCompositeUserReportedIssue) -> Void = { issue in
    // Add a hook to this method, and provide it the Server endpoint + a result callback
    sendSupportEventToServer(server: self.issueUrl, event: issue) { success, result in
        if success {
            // Success: Convey the result link back to the user
        } else {
            // Error: Let the user know something has happened
        }
    }
}

ネットワーク フック

import Foundation
import UIKit
import Combine
import AzureCommunicationUICalling
import Alamofire

/// Sends a support event to a server with details from a `CallCompositeUserReportedIssue`.
/// - Parameters:
///   - server: The URL of the server where the event will be sent.
///   - event: The `CallCompositeUserReportedIssue` containing details about the issue reported by the user.
///   - callback: A closure that is called when the operation is complete.
///               It provides a `Bool` indicating success or failure, and a `String`
///               containing the server's response or an error message.
func sendSupportEventToServer(server: String,
                              event: CallCompositeUserReportedIssue,
                              callback: @escaping (Bool, String) -> Void) {
    // Construct the URL for the endpoint.
    let url = "\(server)/receiveEvent" // Ensure this is replaced with the actual server URL.

    // Extract debugging information from the event.
    let debugInfo = event.debugInfo

    // Prepare the data to be sent as key-value pairs.
    let parameters: [String: String] = [
        "user_message": event.userMessage, // User's message about the issue.
        "ui_version": debugInfo.versions.callingUIVersion, // Version of the calling UI.
        "call_history": debugInfo.callHistoryRecords
            .map { $0.callIds.joined(separator: ",") }
            .joined(separator: "\n") // Call history, formatted.
    ]

    // Define the headers for the HTTP request.
    let headers: HTTPHeaders = [
        .contentType("multipart/form-data")
    ]

    // Perform the multipart/form-data upload.
    AF.upload(multipartFormData: { multipartFormData in
        // Append each parameter as a part of the form data.
        for (key, value) in parameters {
            if let data = value.data(using: .utf8) {
                multipartFormData.append(data, withName: key)
            }
        }

        // Append log files.
        debugInfo.logFiles.forEach { fileURL in
            do {
                let fileData = try Data(contentsOf: fileURL)
                multipartFormData.append(fileData,
                                         withName: "log_files",
                                         fileName: fileURL.lastPathComponent,
                                         mimeType: "application/octet-stream")
            } catch {
                print("Error reading file data: \(error)")
            }
        }
    }, to: url, method: .post, headers: headers).response { response in
        // Handle the response from the server.
        switch response.result {
        case .success(let responseData):
            // Attempt to decode the response.
            if let data = responseData, let responseString = String(data: data, encoding: .utf8) {
                callback(true, responseString) // Success case.
            } else {
                callback(false, "Failed to decode response.") // Failed to decode.
            }
        case .failure(let error):
            // Handle any errors that occurred during the request.
            print("Error sending support event: \(error)")
            callback(false, "Error sending support event: \(error.localizedDescription)")
        }
    }
}

この Swift コードは、Azure Communication Services を使用して iOS アプリケーションからユーザーから報告された問題を送信するプロセスを示しています。 ユーザー フィードバックの収集、診断情報のパッケージ化、およびサーバー エンドポイントへの非同期送信を処理します。 さらに、フィードバック メカニズムを実装するための基盤が提供され、レポートの状態についてユーザーに通知され、アプリケーションの全体的な信頼性とユーザーの満足度が向上します。

Conclusion

Azure Communication Services (ACS) を使用してユーザー フィードバック メカニズムをアプリケーションに統合することは、応答性が高く、ユーザーに焦点を当てたアプリを開発するために不可欠です。 このガイドでは、Android および iOS アプリケーションの Node.js とクライアント側のフィードバック キャプチャを使用してサーバー側の処理を設定するための明確な経路を提供します。 このような統合により、開発者は、効率的なデータ管理のために Azure のクラウド サービスを利用しながら、アプリケーションの信頼性とユーザー満足度を向上させることができます。

このガイドでは、アプリケーションから直接ユーザー フィードバック、エラー ログ、サポート要求をキャプチャするための実用的な手順について説明します。 サポート イベントの統合により、フィードバックを処理するための安全で整理された方法が保証され、開発者はユーザーの問題に迅速に対処して解決でき、全体的なユーザー エクスペリエンスが向上します。

このガイドで詳しく説明されている手順に従うことで、開発者はアプリケーションの応答性を向上させ、ユーザーのニーズをより適切に満たすことができます。 これらの統合は、ユーザーのフィードバックをより効果的に理解するのに役立つだけでなく、クラウド サービスを利用して、スムーズで効果的なフィードバック収集と処理メカニズムを保証します。 最終的には、ユーザーの満足度を優先する魅力的で信頼性の高いアプリケーションを作成するには、ユーザー フィードバック メカニズムの統合が不可欠です。