API 참조
NetFUNNEL Android 에이전트 함수, 콜백 및 응답 형식에 대한 완전한 참조입니다.
목차
초기화 함수
Netfunnel.initialize
목적: 필수 구성을 사용하여 NetFUNNEL Android 에이전트를 초기화합니다.
함수 시그니처:
- Kotlin
- Java
Netfunnel.initialize(
clientId: String,
networkTimeout: Long = 3000,
retryCount: Int = 0,
printLog: Boolean = false,
errorBypass: Boolean = false,
useNetfunnelTemplate: Boolean = true,
userId: String? = null,
useNetworkRecoveryMode: Boolean = false,
statusBarStyle: String? = null
)
Netfunnel.INSTANCE.initialize(
String clientId,
long networkTimeout,
int retryCount,
boolean printLog,
boolean errorBypass,
boolean useNetfunnelTemplate,
String userId,
boolean useNetworkRecoveryMode,
String statusBarStyle
)
빠른 매개변수 개요:
| 카테고리 | 매개변수 | 목적 |
|---|---|---|
| 필수 | clientId | 필수 클라이언트 구성 |
| 네트워크 | networkTimeout, retryCount, useNetworkRecoveryMode | 네트워크 동작 및 안정성 |
| 디버깅 | printLog | 디버그 로깅 활성화 |
| 에러 처리 | errorBypass | 에러 동작 제어 |
| UI/템플릿 | useNetfunnelTemplate, statusBarStyle | 대기실 외관 |
| 고급 | userId | 화이트리스트/블랙리스트용 사용자 식별 |
기본 예제:
- Kotlin
- Java
Netfunnel.initialize(
clientId = "{{CLIENT_ID}}"
)
Netfunnel.INSTANCE.initialize(
"{{CLIENT_ID}}"
);
모든 옵션이 포함된 완전한 예제:
- Kotlin
- Java
Netfunnel.initialize(
clientId = "{{CLIENT_ID}}",
networkTimeout = 5000,
retryCount = 2,
printLog = true,
errorBypass = false,
useNetfunnelTemplate = true,
userId = "user_12345",
useNetworkRecoveryMode = true,
statusBarStyle = "auto"
)
Netfunnel.INSTANCE.initialize(
"{{CLIENT_ID}}",
5000, // networkTimeout
2, // retryCount
true, // printLog
false, // errorBypass
true, // useNetfunnelTemplate
"user_12345", // userId
true, // useNetworkRecoveryMode
"auto" // statusBarStyle
);
모든 초기화 매개변수, 범위, 동작 및 사용 예제에 대한 자세한 정보는 설정 옵션 참조를 참조하세요.
기본 제어 함수
기본 제어는 서비스에 대한 진입 속도를 제한합니다. 시작 함수가 호출되면 키가 발급되고 대기실이 나타납니다. 중지 함수가 호출되면 키가 반환됩니다.
nfStart
목적: 기본 제어를 위해 키를 발급하고 대기실을 표시합니다.
함수 시그니처:
- Kotlin
- Java
Netfunnel.nfStart(
projectKey: String,
segmentKey: String,
callback: NetfunnelCallback,
activity: Activity
)
Netfunnel.INSTANCE.nfStart(
String projectKey,
String segmentKey,
NetfunnelCallback callback,
Activity activity
)
매개변수:
| 매개변수 | 타입 | 설명 | 필수 |
|---|---|---|---|
projectKey | String | 콘솔의 기본 제어 프로젝트 키 | 예 |
segmentKey | String | 콘솔의 기본 제어 세그먼트 키 | 예 |
callback | NetfunnelCallback | VWR 이벤트를 처리할 콜백 | 예 |
activity | Activity | 현재 activity context | 예 |
반환값: void
예제:
- Kotlin
- Java
Netfunnel.nfStart(
projectKey = "service_1",
segmentKey = "segKey_8612",
callback = myCallback,
activity = this
)
Netfunnel.INSTANCE.nfStart(
"service_1",
"segKey_8612",
myCallback,
this
);
nfStop
목적: 진입이 완료된 후 키를 반환합니다.
함수 시그니처:
- Kotlin
- Java
Netfunnel.nfStop(
projectKey: String,
segmentKey: String,
completeCallback: NetfunnelCompleteCallback? = null
)
Netfunnel.INSTANCE.nfStop(
String projectKey,
String segmentKey,
NetfunnelCompleteCallback completeCallback
)
매개변수:
| 매개변수 | 타입 | 설명 | 필수 |
|---|---|---|---|
projectKey | String | 콘솔의 기본 제어 프로젝트 키 | 예 |
segmentKey | String | 콘솔의 기본 제어 세그먼트 키 | 예 |
completeCallback | NetfunnelCompleteCallback? | 키 반환 확인을 위한 콜백 | 아니오 |
반환값: void
예제:
- Kotlin
- Java
Netfunnel.nfStop(
projectKey = "service_1",
segmentKey = "segKey_8612",
completeCallback = myCompleteCallback
)
Netfunnel.INSTANCE.nfStop(
"service_1",
"segKey_8612",
myCompleteCallback
);
중지 함수를 실행하지 않으면 세그먼트 타임아웃 설정에 따라 키가 자동으로 반환됩니다 (기본 타임아웃: 20초).
구간 제어 함수
구간 제어는 특정 구간에서 고정된 수의 동시 사용자를 유지합니다. 시작 함수가 호출되면 키가 발급됩니다. 중지 함수가 호출될 때까지 사용자는 활성 구간에 있는 것으로 간주되며, 대기열의 다음 사용자는 허용되지 않습니다.
사용 사례:
- 다단계 프로세스: 이벤트 페이지 진입 후, 제품 구매 및 결제 완료 버튼 클릭까지
- 사용자 세션 관리: 사용자 로그인부터 로그아웃까지
- 리소스 집약적인 작업: 특정 기능에 대한 최적 사용자 수 유지
nfStartSection
목적: 구간 제어를 위해 키를 발급하고 대기실을 표시합니다.
함수 시그니처:
- Kotlin
- Java
Netfunnel.nfStartSection(
projectKey: String,
segmentKey: String,
callback: NetfunnelCallback,
activity: Activity
)
Netfunnel.INSTANCE.nfStartSection(
String projectKey,
String segmentKey,
NetfunnelCallback callback,
Activity activity
)
매개변수:
| 매개변수 | 타입 | 설명 | 필수 |
|---|---|---|---|
projectKey | String | 콘솔의 구간 제어 프로젝트 키 | 예 |
segmentKey | String | 콘솔의 구간 제어 세그먼트 키 | 예 |
callback | NetfunnelCallback | VWR 이벤트를 처리할 콜백 | 예 |
activity | Activity | 현재 activity context | 예 |
반환값: void
예제:
- Kotlin
- Java
Netfunnel.nfStartSection(
projectKey = "service_1",
segmentKey = "section_segKey_1234",
callback = myCallback,
activity = this
)
Netfunnel.INSTANCE.nfStartSection(
"service_1",
"section_segKey_1234",
myCallback,
this
);
nfStopSection
목적: 사용자가 활성 구간을 종료할 때 키를 반환합니다.
함수 시그니처:
- Kotlin
- Java
Netfunnel.nfStopSection(
projectKey: String,
segmentKey: String,
completeCallback: NetfunnelCompleteCallback? = null
)
Netfunnel.INSTANCE.nfStopSection(
String projectKey,
String segmentKey,
NetfunnelCompleteCallback completeCallback
)
매개변수:
| 매개변수 | 타입 | 설명 | 필수 |
|---|---|---|---|
projectKey | String | 콘솔의 구간 제어 프로젝트 키 | 예 |
segmentKey | String | 콘솔의 구간 제어 세그먼트 키 | 예 |
completeCallback | NetfunnelCompleteCallback? | 키 반환 확인을 위한 콜백 | 아니오 |
반환값: void
예제:
- Kotlin
- Java
Netfunnel.nfStopSection(
projectKey = "service_1",
segmentKey = "section_segKey_1234",
completeCallback = myCompleteCallback
)
Netfunnel.INSTANCE.nfStopSection(
"service_1",
"section_segKey_1234",
myCompleteCallback
);
중지 함수를 실행하지 않으면 사용자가 활성 구간에 머물러 있는 것으로 간주되며, 다음 사용자의 진입이 지연될 수 있습니다.
콜백 클래스
NetfunnelCallback
목적: 시작 함수에 대한 NetFUNNEL 서버 응답을 처리합니다.
Android에서 NetfunnelCallback은 모든 메서드를 구현해야 하는 인터페이스입니다. 일부 메서드만 구현할 수 없습니다 - 모든 콜백 메서드에 대한 구현을 제공해야 합니다. 그러나 일부 메서드는 다른 메서드보다 더 중요합니다. 기본 구현의 경우 선택적 메서드에 대해 TODO("Not yet implemented")를 사용할 수 있지만, 프로덕션 코드에서는 이를 적절한 구현으로 교체해야 합니다.
추상 메서드:
| 메서드 | 필수 | 설명 | 구현 전략 |
|---|---|---|---|
onSuccess | 예 | 진입이 허용되거나 우회될 때 호출됨 | 항상 구현 - 핵심 비즈니스 로직 |
onError | 예 | 시스템 에러가 발생할 때 호출됨 | 항상 구현 - 일반적으로 비즈니스 로직 진행 |
onNetworkError | 예 | 네트워크 연결 문제가 발생할 때 호출됨 | 항상 구현 - 재시도 또는 비즈니스 로직 진행 |
onBlock | 선택 | 사용자가 차단될 때 호출됨 | 초기에는 TODO 사용 가능 - 차단 메시지 표시 |
onClose | 선택 | 사용자가 대기실을 닫을 때 호출됨 | 초기에는 TODO 사용 가능 - 사용자 취소 처리 |
onContinue | 선택 | 사용자 정의 대기실 업데이트를 위해 호출됨 | 초기에는 TODO 사용 가능 - 사용자 정의 대기실에만 필요 |
기본 구현 (단계별):
초기 개발의 경우 필수 메서드만 구현하고 나머지는 TODO를 사용할 수 있습니다:
- Kotlin
- Java
import com.nf4.NetfunnelCallback
class StartCallback {
companion object {
private const val TAG = "NetFUNNEL"
}
private val callback = object : NetfunnelCallback() {
override fun onSuccess(statusCode: Int, message: String) {
Log.d(TAG, "onSuccess $statusCode $message")
// 핵심 비즈니스 로직 - 항상 구현
proceedWithAction()
}
override fun onError(statusCode: Int, message: String) {
Log.d(TAG, "onError $statusCode $message")
// 일반적으로 비즈니스 로직 진행 - 항상 구현
proceedWithAction()
}
override fun onNetworkError(statusCode: Int, message: String) {
Log.d(TAG, "onNetworkError $statusCode $message")
// 재시도 또는 비즈니스 로직 진행 - 항상 구현
proceedWithAction()
}
override fun onBlock(statusCode: Int, message: String) {
TODO("Not yet implemented")
}
override fun onClose(statusCode: Int, message: String) {
TODO("Not yet implemented")
}
override fun onContinue(statusCode: Int, message: String, aheadWait: Int, behindWait: Int, waitTime: String, progressRate: Int) {
TODO("Not yet implemented")
}
}
fun getCallback(): NetfunnelCallback = callback
}
import com.nf4.NetfunnelCallback;
public class StartCallback {
private static final String TAG = "NetFUNNEL";
private final NetfunnelCallback callback = new NetfunnelCallback() {
@Override
public void onSuccess(int statusCode, @NonNull String message) {
Log.d(TAG, "onSuccess " + statusCode + " " + message);
// 핵심 비즈니스 로직 - 항상 구현
proceedWithAction();
}
@Override
public void onError(int statusCode, @NonNull String message) {
Log.d(TAG, "onError " + statusCode + " " + message);
// 일반적으로 비즈니스 로직 진행 - 항상 구현
proceedWithAction();
}
@Override
public void onNetworkError(int statusCode, @NonNull String message) {
Log.d(TAG, "onNetworkError " + statusCode + " " + message);
// 재시도 또는 비즈니스 로직 진행 - 항상 구현
proceedWithAction();
}
@Override
public void onBlock(int statusCode, @NonNull String message) {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public void onClose(int statusCode, @NonNull String message) {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public void onContinue(int statusCode, @NonNull String message, int aheadWait, int behindWait, @NonNull String waitTime, int progressRate) {
throw new UnsupportedOperationException("Not yet implemented");
}
};
public NetfunnelCallback getCallback() {
return callback;
}
}
완전한 구현 (프로덕션 준비):
프로덕션 코드의 경우 모든 메서드를 적절한 처리와 함께 구현합니다:
- Kotlin
- Java
import com.nf4.NetfunnelCallback
class StartCallback {
companion object {
private const val TAG = "NetFUNNEL"
}
private val callback = object : NetfunnelCallback() {
override fun onSuccess(statusCode: Int, message: String) {
Log.d(TAG, "onSuccess $statusCode $message")
/**
* 대기열 통과 처리 로직
* 예 - 서비스 화면 진입 처리
*/
proceedWithAction()
}
override fun onError(statusCode: Int, message: String) {
Log.d(TAG, "onError $statusCode $message")
/**
* 시스템 오류 처리 로직 - 서비스 가용성을 유지하기 위해 비즈니스 로직 진행
* 예 - 서버 오류는 일반적으로 일시적이며 사용자 액세스를 차단하지 않아야 함
*/
proceedWithAction()
}
override fun onNetworkError(statusCode: Int, message: String) {
Log.d(TAG, "onNetworkError $statusCode $message")
/**
* 네트워크 오류 처리 로직 - 로깅만 수행, 비즈니스 로직은 실행하지 않음
* 예 - 자동 네트워크 복구를 위해 useNetworkRecoveryMode = true 사용
*/
// 참고: 여기서는 비즈니스 로직을 실행하지 않음 - 네트워크 복구 모드에 의존
}
override fun onBlock(statusCode: Int, message: String) {
Log.d(TAG, "onBlock $statusCode $message")
/**
* 사용자 진입 차단 처리 로직
* 예 - 액세스 제한 메시지 표시
*/
showBlockedMessage()
}
override fun onClose(statusCode: Int, message: String) {
Log.d(TAG, "onClose $statusCode $message")
/**
* 사용자 취소 처리 로직 (WebView가 자동으로 닫히고 이전 화면으로 돌아감)
* 예 - 종료 알림 Toast 표시
*/
handleUserCancel()
}
override fun onContinue(statusCode: Int, message: String, aheadWait: Int, behindWait: Int, waitTime: String, progressRate: Int) {
Log.d(TAG, "onContinue $statusCode $message")
/**
* 대기 진행 상황 UI 업데이트 로직 (사용자 정의 대기실 사용 시에만 적용)
* 예 - 사용자 정의 대기 화면에 실시간 대기 정보 업데이트
*/
updateCustomWaitingRoom(aheadWait, behindWait, waitTime, progressRate)
}
}
fun getCallback(): NetfunnelCallback = callback
}
import com.nf4.NetfunnelCallback;
public class StartCallback {
private static final String TAG = "NetFUNNEL";
private final NetfunnelCallback callback = new NetfunnelCallback() {
@Override
public void onSuccess(int statusCode, @NonNull String message) {
Log.d(TAG, "onSuccess " + statusCode + " " + message);
/**
* 대기열 통과 처리 로직
* 예 - 서비스 화면 진입 처리
*/
proceedWithAction();
}
@Override
public void onError(int statusCode, @NonNull String message) {
Log.d(TAG, "onError " + statusCode + " " + message);
/**
* 시스템 오류 처리 로직 - 서비스 가용성을 유지하기 위해 비즈니스 로직 진행
* 예 - 서버 오류는 일반적으로 일시적이며 사용자 액세스를 차단하지 않아야 함
*/
proceedWithAction();
}
@Override
public void onNetworkError(int statusCode, @NonNull String message) {
Log.d(TAG, "onNetworkError " + statusCode + " " + message);
/**
* 네트워크 오류 처리 로직 - 로깅만 수행, 비즈니스 로직은 실행하지 않음
* 예 - 자동 네트워크 복구를 위해 useNetworkRecoveryMode = true 사용
*/
// 참고: 여기서는 비즈니스 로직을 실행하지 않음 - 네트워크 복구 모드에 의존
}
@Override
public void onBlock(int statusCode, @NonNull String message) {
Log.d(TAG, "onBlock " + statusCode + " " + message);
/**
* 사용자 진입 차단 처리 로직
* 예 - 액세스 제한 메시지 표시
*/
showBlockedMessage();
}
@Override
public void onClose(int statusCode, @NonNull String message) {
Log.d(TAG, "onClose " + statusCode + " " + message);
/**
* 사용자 취소 처리 로직 (WebView가 자동으로 닫히고 이전 화면으로 돌아감)
* 예 - 종료 알림 Toast 표시
*/
handleUserCancel();
}
@Override
public void onContinue(int statusCode, @NonNull String message, int aheadWait, int behindWait, @NonNull String waitTime, int progressRate) {
Log.d(TAG, "onContinue " + statusCode + " " + message);
/**
* 대기 진행 상황 UI 업데이트 로직 (사용자 정의 대기실 사용 시에만 적용)
* 예 - 사용자 정의 대기 화면에 실시간 대기 정보 업데이트
*/
updateCustomWaitingRoom(aheadWait, behindWait, waitTime, progressRate);
}
};
public NetfunnelCallback getCallback() {
return callback;
}
}
응답 처리 전략:
| 응답 유형 | 상태 코드 | 동작 | 비즈니스 로직 | 사용자 알림 |
|---|---|---|---|---|
| Success | 200, 300, 303 | 실행 | ✅ 예 | 선택 사항 |
| Error | 500 | 실행 | ✅ 예 | 선택 사항 |
| NetworkError | 1001, 1002 | 로깅만 | ❌ 아니오 | 선택 사항 |
| Block | 301, 302 | 중지 | ❌ 아니오 | 필수 |
| Close | 495-499 | 중지 | ❌ 아니오 | 선택 사항 |
onError는 비즈니스 로직을 실행하지만 onNetworkError는 실행하지 않는 이유:
onError (상태 코드 500) - 서버 오류:
- 시나리오: NetFUNNEL 서버가 내부 오류를 만남
- 전략: 서비스 가용성을 유지하기 위해 비즈니스 로직 실행
- 근거: 서버 오류는 일반적으로 일시적이며 사용자 액세스를 차단하지 않아야 함
- 결과: NetFUNNEL에 문제가 있어도 계속되는 견고한 서비스
onNetworkError (상태 코드 1001, 1002) - 네트워크 문제:
- 시나리오: 네트워크 연결 문제 (오프라인, 타임아웃)
- 전략: 로깅만 수행, 비즈니스 로직 실행 안 함
- 근거: 자동 네트워크 복구를 위해
useNetworkRecoveryMode = true사용 - 결과: 네트워크 문제 중 사용자가 대기실에 머물고 연결이 복구되면 자동 재개
네트워크 복구 모드 구성:
최적의 네트워크 오류 처리를 위해 Application 클래스에서 네트워크 복구를 활성화합니다:
- Kotlin
- Java
// SampleApplication.kt
Netfunnel.initialize(
clientId = "{{CLIENT_ID}}",
useNetworkRecoveryMode = true // 자동 네트워크 복구 활성화
)
// SampleApplication.java
Netfunnel.INSTANCE.initialize(
"{{CLIENT_ID}}",
3000, // networkTimeout
0, // retryCount
false, // printLog
false, // errorBypass
true, // useNetfunnelTemplate
null, // userId
true, // useNetworkRecoveryMode
null // statusBarStyle
);
NetfunnelCompleteCallback
목적: 중지 함수에 대한 NetFUNNEL 서버 응답을 처리합니다.
추상 메서드:
| 메서드 | 필수 | 설명 |
|---|---|---|
onComplete | 아니오 | 키 반환이 완료될 때 호출됨 |
예제 구현:
- Kotlin
- Java
import com.nf4.NetfunnelCompleteCallback
class StopCallback {
companion object {
private const val TAG = "NetFUNNEL"
}
private val callback = object : NetfunnelCompleteCallback() {
override fun onComplete(statusCode: Int, message: String) {
Log.d(TAG, "onComplete $statusCode $message")
/**
* 진입 키 반환 결과 처리 로직
* 예 - 키 반환이 성공하면 다음 페이지로 이동
*/
handleKeyReturnCompletion()
}
}
fun getCallback(): NetfunnelCompleteCallback = callback
}
import com.nf4.NetfunnelCompleteCallback;
public class StopCallback {
private static final String TAG = "NetFUNNEL";
private final NetfunnelCompleteCallback callback = new NetfunnelCompleteCallback() {
@Override
public void onComplete(int statusCode, @NonNull String message) {
Log.d(TAG, "onComplete " + statusCode + " " + message);
/**
* 진입 키 반환 결과 처리 로직
* 예 - 키 반환이 성공하면 다음 페이지로 이동
*/
handleKeyReturnCompletion();
}
};
public NetfunnelCompleteCallback getCallback() {
return callback;
}
}
응답 객체 스키마
콜백 메서드는 다음 매개변수를 받습니다:
| 매개변수 | 타입 | 설명 | 예제 |
|---|---|---|---|
statusCode | Int | HTTP 상태 코드 | 200, 300, 500, 1001 등 |
message | String | 응답 메시지 | "Success", "Bypass", "Server Error" 등 |
aheadWait | Int | 대기열 앞의 사용자 수 (onContinue만) | 5 |
behindWait | Int | 대기열 뒤의 사용자 수 (onContinue만) | 10 |
waitTime | String | 예상 대기 시간 (onContinue만) | "2 minutes" |
progressRate | Int | 진행률 백분율 (onContinue만) | 75 |
상태 코드 참조
가능한 모든 상태 코드와 그 의미에 대한 완전한 참조입니다.
| 상태 | StatusCode | Message | 설명 |
|---|---|---|---|
| Success | 200 | Success | 대기열을 성공적으로 통과하고 서비스에 진입함. 정상적인 대기열 통과. |
| 300 | Bypass | 다양한 조건으로 인해 진입이 우회됨. 예: 구독/라이선스 만료, 콘솔에서 프로젝트/세그먼트 비활성화, errorBypass=true 설정 및 에러 발생. | |
| 303 | Express | 익스프레스 진입 성공. 콘솔 화이트리스트에 등록된 IP 또는 ID (관리자 우회). | |
| Error | 500 | Server Error | 시스템 에러 발생. 예: 시작 함수 전에 에이전트 초기화 함수가 호출되지 않음, 존재하지 않는 프로젝트/세그먼트 키 사용, 콘솔에서 세그먼트 삭제됨, 부분 응답 손실을 유발하는 서버 에러. |
| NetworkError | 1001 | Network Not Connected | 네트워크 연결 차단됨. WiFi/셀룰러 데이터 비활성화. |
| 1002 | Network Timeout | 네트워크 타임아웃 발생. 예: 네트워크 지연, 잘못된 대기실 HTML URL 수신, 서버 다운 (502 등). | |
| Block | 301 | Block | 세그먼트가 차단 상태임. 콘솔에서 세그먼트 차단됨 (선의의 진입 차단). |
| 302 | Macro Block | 사용자 차단됨. 예: 콘솔 블랙리스트에 등록된 IP 또는 ID (관리자 차단), BotManager Basic 활성화됨 (악의적인 진입 차단). | |
| Close | 495 | Closed PostWaiting Room | 사후 대기실 닫힘. 사용자가 사후 대기실에서 닫기 버튼 클릭. |
| 496 | Closed PreWaiting Room | 사전 대기실 닫힘. 사용자가 사전 대기실에서 닫기 버튼 클릭. | |
| 497 | Closed Macro Blocking Room | 매크로 차단실 닫힘. 사용자가 매크로 차단실에서 닫기 버튼 클릭. | |
| 498 | Closed Blocking Room | 차단실 닫힘. 사용자가 차단실에서 닫기 버튼 클릭. | |
| 499 | Canceled Waiting Room | 기본 대기실에서 취소/닫기 버튼 클릭. 사용자가 기본 대기실에서 취소 버튼 클릭. | |
| Continue | 201 | Continue | 사용자 정의 템플릿을 위한 대기실 업데이트. 에이전트 useNetfunnelTemplate=false 설정 및 기본 대기 중. |
유틸리티 함수
getVersion
목적: NetFUNNEL Android 에이전트의 현재 버전을 가져옵니다.
함수 시그니처:
- Kotlin
- Java
Netfunnel.getVersion(): String
Netfunnel.INSTANCE.getVersion(): String
반환값: String - NetFUNNEL Android 에이전트의 버전 문자열
예제:
- Kotlin
- Java
val version = Netfunnel.getVersion()
Log.d("NetFUNNEL", "Agent version: $version")
String version = Netfunnel.INSTANCE.getVersion();
Log.d("NetFUNNEL", "Agent version: " + version);