Debugging Null wrapUpCode Values in Genesys Cloud Analytics Detail Queries
What You Will Build
- A Python script that queries Genesys Cloud Analytics Conversation Details and correctly interprets
wrapUpCodefields. - Code that distinguishes between missing data, empty data, and actual wrap-up codes using the Genesys Cloud Python SDK.
- A diagnostic routine to identify if a null value stems from a query filter error, a data latency issue, or a specific conversation type limitation.
Prerequisites
- OAuth Client: Service Account with
analytics:conversation:readscope. - SDK:
genesys-cloud-sdk-pythonversion 120.0.0 or later. - Language: Python 3.8+.
- Dependencies:
pip install genesys-cloud-sdk-python.
Authentication Setup
Genesys Cloud uses OAuth 2.0. For server-side scripts, the Client Credentials flow is the standard. You must initialize the PureCloudPlatformClientV2 with your environment URL and credentials.
import os
from purecloudplatform.client import PureCloudPlatformClientV2
def get_platform_client() -> PureCloudPlatformClientV2:
"""
Initializes and returns an authenticated Genesys Cloud Platform Client.
"""
# Load credentials from environment variables to avoid hardcoding secrets
environment_url = os.getenv("GENESYS_ENVIRONMENT_URL", "https://api.mypurecloud.com")
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set in environment.")
platform_client = PureCloudPlatformClientV2(environment_url)
platform_client.login_client_credentials(client_id, client_secret)
return platform_client
Implementation
Step 1: Constructing the Detail Query
The root cause of “null” wrap-up codes is often an incorrect query specification. The wrapUpCode field is only populated for conversations that have reached a wrapped or completed state. If you query for queued or in-progress conversations, this field will be null.
You must explicitly select the wrapUpCode field in the select clause. If omitted, the API may not return the field at all, or return it as null depending on the endpoint version.
from purecloudplatform.models import ConversationDetailsQuery
def create_detail_query(platform_client: PureCloudPlatformClientV2) -> dict:
"""
Constructs a query body that targets completed conversations and selects wrapUpCode.
"""
analytics_api = platform_client.AnalyticsApi()
# Define the time range: Last 24 hours
import datetime
end_time = datetime.datetime.utcnow()
start_time = end_time - datetime.timedelta(hours=24)
# Construct the query body
query_body = ConversationDetailsQuery(
select=[
"id",
"type",
"start_time",
"end_time",
"wrap_up_code", # Critical: Must be explicitly selected
"wrap_up_code_description",
"agents" # To see which agent handled it
],
where=[
{
"path": "type",
"operator": "in",
"value": ["voice", "email"] # Limit to types that support wrap-up
},
{
"path": "status",
"operator": "eq",
"value": "completed" # Crucial: Wrap-up only exists after completion
}
],
time_range={
"start": start_time.isoformat() + "Z",
"end": end_time.isoformat() + "Z"
},
page_size=100
)
return query_body
Step 2: Executing the Query and Handling Pagination
Analytics queries return a ConversationDetailsResponse. This object contains a conversation_details list. You must iterate through this list. If the response returns 429 (Too Many Requests), you must implement exponential backoff.
import time
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def fetch_wrap_up_data(platform_client: PureCloudPlatformClientV2, query_body: dict):
"""
Executes the detail query with retry logic for 429 errors.
"""
analytics_api = platform_client.AnalyticsApi()
all_conversations = []
while True:
try:
response = analytics_api.post_analytics_conversations_details_query(
body=query_body,
async_req=False
)
if response.conversation_details:
all_conversations.extend(response.conversation_details)
# Check for next page
if not response.next_page_uri:
break
# Update the query body with the next page URI
query_body.next_page_token = response.next_page_token.replace("/api/v2/analytics/conversations/details/query?", "")
except Exception as e:
status_code = e.status_code if hasattr(e, 'status_code') else None
if status_code == 429:
wait_time = min(60, 2 ** (int(e.headers.get('Retry-After', 1))))
logger.warning(f"Rate limited (429). Waiting {wait_time} seconds.")
time.sleep(wait_time)
else:
logger.error(f"API Error: {e}")
raise
return all_conversations
Step 3: Analyzing Null Values
Once you have the data, you must inspect the wrap_up_code field. A null value here can mean three things:
- The conversation was abandoned: The agent never wrapped it up because the user hung up or the system auto-disposed it.
- The wrap-up code was not required: Some routing strategies or skills do not mandate a wrap-up code.
- Data Latency: The conversation ended, but the analytics pipeline has not yet finalized the record.
This script filters for nulls and prints diagnostic context.
def analyze_null_wrap_ups(conversations: list):
"""
Inspects conversations where wrap_up_code is null.
"""
null_wrap_ups = []
for conv in conversations:
# Access the wrap_up_code safely
# In the SDK, this is usually a string or None
wrap_code = conv.wrap_up_code if hasattr(conv, 'wrap_up_code') else None
if wrap_code is None or wrap_code == "":
null_wrap_ups.append({
"id": conv.id,
"type": conv.type,
"status": conv.status,
"end_time": conv.end_time,
"agents": conv.agents
})
logger.info(f"Total conversations: {len(conversations)}")
logger.info(f"Conversations with null/empty wrap-up: {len(null_wrap_ups)}")
for item in null_wrap_ups:
logger.info(f"ID: {item['id']} | Type: {item['type']} | Status: {item['status']}")
# Check if agents were present. If no agents, it was likely an abandoned call.
if not item['agents'] or len(item['agents']) == 0:
logger.info(" -> Reason: No agents involved (Abandoned/IVR only)")
else:
logger.info(" -> Reason: Agent handled but no wrap-up code recorded")
return null_wrap_ups
Complete Working Example
This script combines authentication, query construction, execution, and analysis into a single runnable module.
import os
import datetime
import logging
from purecloudplatform.client import PureCloudPlatformClientV2
from purecloudplatform.models import ConversationDetailsQuery
# Configure Logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def main():
# 1. Authenticate
try:
platform_client = PureCloudPlatformClientV2(os.getenv("GENESYS_ENVIRONMENT_URL", "https://api.mypurecloud.com"))
platform_client.login_client_credentials(
os.getenv("GENESYS_CLIENT_ID"),
os.getenv("GENESYS_CLIENT_SECRET")
)
logger.info("Authentication successful.")
except Exception as e:
logger.error(f"Authentication failed: {e}")
return
# 2. Build Query
end_time = datetime.datetime.utcnow()
start_time = end_time - datetime.timedelta(hours=24)
query_body = ConversationDetailsQuery(
select=[
"id",
"type",
"status",
"end_time",
"wrap_up_code",
"wrap_up_code_description",
"agents"
],
where=[
{
"path": "type",
"operator": "in",
"value": ["voice", "email", "chat", "callback"]
},
{
"path": "status",
"operator": "eq",
"value": "completed"
}
],
time_range={
"start": start_time.isoformat() + "Z",
"end": end_time.isoformat() + "Z"
},
page_size=500
)
# 3. Fetch Data
try:
logger.info("Fetching conversation details...")
conversations = fetch_wrap_up_data(platform_client, query_body)
if not conversations:
logger.info("No conversations found in the specified time range.")
return
logger.info(f"Retrieved {len(conversations)} conversations.")
# 4. Analyze Nulls
null_items = analyze_null_wrap_ups(conversations)
if null_items:
logger.warning("Found conversations with missing wrap-up codes. Check logs for details.")
else:
logger.info("All retrieved completed conversations have valid wrap-up codes.")
except Exception as e:
logger.error(f"Error during execution: {e}")
def fetch_wrap_up_data(platform_client: PureCloudPlatformClientV2, query_body: dict):
analytics_api = platform_client.AnalyticsApi()
all_conversations = []
while True:
try:
response = analytics_api.post_analytics_conversations_details_query(
body=query_body,
async_req=False
)
if response.conversation_details:
all_conversations.extend(response.conversation_details)
if not response.next_page_uri:
break
# Parse next page token from URI
next_uri = response.next_page_uri
if next_uri:
query_body.next_page_token = next_uri.split("next_page_token=")[-1]
except Exception as e:
status_code = e.status_code if hasattr(e, 'status_code') else None
if status_code == 429:
wait_time = int(e.headers.get('Retry-After', 1))
logger.warning(f"Rate limited. Waiting {wait_time}s.")
import time
time.sleep(wait_time)
else:
raise
return all_conversations
def analyze_null_wrap_ups(conversations: list):
null_wrap_ups = []
for conv in conversations:
wrap_code = getattr(conv, 'wrap_up_code', None)
if not wrap_code:
null_wrap_ups.append({
"id": conv.id,
"type": conv.type,
"status": conv.status,
"agents": conv.agents
})
logger.info(f"Null Wrap-Ups: {len(null_wrap_ups)} / {len(conversations)}")
for item in null_wrap_ups[:5]: # Show first 5
logger.info(f"ID: {item['id']} | Type: {item['type']} | Agents: {len(item['agents']) if item['agents'] else 0}")
return null_wrap_ups
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 400 Bad Request - Invalid Query
Cause: The where clause contains a field that is not indexable or the syntax is incorrect. For example, using eq on a field that requires in.
Fix: Verify the operator for the field. The status field often requires eq or in. The type field requires in.
{
"where": [
{
"path": "status",
"operator": "eq",
"value": "completed"
}
]
}
Error: wrapUpCode is Null for Completed Conversations
Cause: This is not a query error. It is a data reality.
- Abandoned Calls: If the
agentsarray is empty, the call was never answered. Wrap-up codes do not exist for abandoned calls. - No Wrap-up Required: If the routing strategy did not enforce a wrap-up code, the agent could close the interaction without selecting one.
- System Disposed: If the interaction ended due to a system timeout or error, the wrap-up code may be null.
Fix: Filter out conversations where agents is empty. If agents are present and the status is completed but wrap-up is null, check your Genesys Cloud administration settings for “Wrap-up Code Required” on the specific skill or queue.
Error: 401 Unauthorized
Cause: The OAuth token has expired or the client credentials are invalid.
Fix: Ensure GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET are correct. The SDK handles token refresh automatically, but if the initial login fails, check the credentials.
# Verify scope
platform_client.login_client_credentials(client_id, client_secret)
# Ensure the client has analytics:conversation:read scope in Genesys Admin