Commit 43b69949ec for asterisk.org

commit 43b69949ec650e8c05e1ed0e5d37be31f860b912
Author: phoneben <3232963@gmail.com>
Date:   Sun Jan 18 20:34:01 2026 +0200

    app_queue: Queue Timing Parity with Dial() and Accurate Wait Metrics

    app_queue: Set Dial-compatible timing variables

    Extends Queue() to set Dial-compatible timing variables (ANSWEREDTIME, DIALEDTIME) and introduces a precise QUEUEWAIT metric calculated at agent connect time, with proper initialization to prevent stale or misleading values.

diff --git a/apps/app_queue.c b/apps/app_queue.c
index 3233f384f1..fdb426aa1b 100644
--- a/apps/app_queue.c
+++ b/apps/app_queue.c
@@ -71,6 +71,7 @@
 #include <signal.h>
 #include <netinet/in.h>
 #include <ctype.h>
+#include <inttypes.h>

 #include "asterisk/lock.h"
 #include "asterisk/file.h"
@@ -303,6 +304,24 @@
 				<variable name="QUEUE_WITHDRAW_INFO">
 					<para>If the call was successfully withdrawn from the queue, and the withdraw request was provided with optional withdraw info, the withdraw info will be stored in this variable.</para>
 				</variable>
+				<variable name="QUEUEWAIT">
+					<para>The total amount of time, in seconds, that the caller spent waiting in the queue before being connected to an agent.</para>
+				</variable>
+				<variable name="QUEUEWAIT_MS">
+					<para>The total amount of time, in milliseconds, that the caller spent waiting in the queue before being connected to an agent.</para>
+				</variable>
+				<variable name="ANSWEREDTIME">
+					<para>The amount of time, in seconds, that the caller spent connected to an agent. If the call was never answered, this will be set to 0.</para>
+				</variable>
+				<variable name="ANSWEREDTIME_MS">
+					<para>The amount of time, in milliseconds, that the caller spent connected to an agent.</para>
+				</variable>
+				<variable name="DIALEDTIME">
+					<para>The total amount of time, in seconds, from the start of the call until it ends. This matches the behavior of Dial().</para>
+				</variable>
+				<variable name="DIALEDTIME_MS">
+					<para>The total amount of time, in milliseconds, from the start of the call until it ends.</para>
+				</variable>
 			</variablelist>
 		</description>
 		<see-also>
@@ -6987,8 +7006,26 @@ static int setup_stasis_subs(struct queue_ent *qe, struct ast_channel *peer, str
 struct queue_end_bridge {
 	struct call_queue *q;
 	struct ast_channel *chan;
+	struct timeval start_time;
 };

+/*!
+ * \internal
+ * \brief Helper to set the standard Dial duration variables
+ */
+static void set_duration_var(struct ast_channel *chan, const char *var_base, int64_t duration)
+{
+	char buf[32];
+	char full_var_name[128];
+
+	snprintf(buf, sizeof(buf), "%" PRId64, duration / 1000);
+	pbx_builtin_setvar_helper(chan, var_base, buf);
+
+	snprintf(full_var_name, sizeof(full_var_name), "%s_MS", var_base);
+	snprintf(buf, sizeof(buf), "%" PRId64, duration);
+	pbx_builtin_setvar_helper(chan, full_var_name, buf);
+}
+
 static void end_bridge_callback_data_fixup(struct ast_bridge_config *bconfig, struct ast_channel *originator, struct ast_channel *terminator)
 {
 	struct queue_end_bridge *qeb = bconfig->end_bridge_callback_data;
@@ -7001,9 +7038,20 @@ static void end_bridge_callback(void *data)
 	struct queue_end_bridge *qeb = data;
 	struct call_queue *q = qeb->q;
 	struct ast_channel *chan = qeb->chan;
+	int64_t answered_time_ms;

 	if (ao2_ref(qeb, -1) == 1) {
 		set_queue_variables(q, chan);
+
+		/* Match Dial() timing variables */
+		ast_channel_lock(chan);
+		ast_channel_stage_snapshot(chan);
+		answered_time_ms = ast_tvdiff_ms(ast_tvnow(), qeb->start_time);
+		set_duration_var(chan, "ANSWEREDTIME", answered_time_ms);
+		set_duration_var(chan, "DIALEDTIME", ast_channel_get_duration_ms(chan));
+		ast_channel_stage_snapshot_done(chan);
+		ast_channel_unlock(chan);
+
 		/* This unrefs the reference we made in try_calling when we allocated qeb */
 		queue_t_unref(q, "Expire bridge_config reference");
 	}
@@ -7560,6 +7608,8 @@ static int try_calling(struct queue_ent *qe, struct ast_flags opts, char **opt_a

 		ast_queue_log(queuename, ast_channel_uniqueid(qe->chan), member->membername, "CONNECT", "%ld|%s|%ld", (long) (time(NULL) - qe->start), ast_channel_uniqueid(peer),
 													(long)(orig - to > 0 ? (orig - to) / 1000 : 0));
+		/* Queue hold time until agent answered */
+		set_duration_var(qe->chan, "QUEUEWAIT", (int64_t)(time(NULL) - qe->start) * 1000);

 		blob = ast_json_pack("{s: s, s: s, s: s, s: I, s: I}",
 				     "Queue", queuename,
@@ -7575,6 +7625,7 @@ static int try_calling(struct queue_ent *qe, struct ast_flags opts, char **opt_a
 		if ((queue_end_bridge = ao2_alloc(sizeof(*queue_end_bridge), NULL))) {
 			queue_end_bridge->q = qe->parent;
 			queue_end_bridge->chan = qe->chan;
+			queue_end_bridge->start_time = ast_tvnow();
 			bridge_config.end_bridge_callback = end_bridge_callback;
 			bridge_config.end_bridge_callback_data = queue_end_bridge;
 			bridge_config.end_bridge_callback_data_fixup = end_bridge_callback_data_fixup;
@@ -8727,6 +8778,13 @@ static int queue_exec(struct ast_channel *chan, const char *data)
 	char *opt_args[OPT_ARG_ARRAY_SIZE];
 	int max_forwards;
 	int cid_allow;
+	/* Reset variables to avoid stale data */
+	pbx_builtin_setvar_helper(chan, "ANSWEREDTIME", "");
+	pbx_builtin_setvar_helper(chan, "ANSWEREDTIME_MS", "");
+	pbx_builtin_setvar_helper(chan, "DIALEDTIME", "");
+	pbx_builtin_setvar_helper(chan, "DIALEDTIME_MS", "");
+	pbx_builtin_setvar_helper(chan, "QUEUEWAIT", "");
+	pbx_builtin_setvar_helper(chan, "QUEUEWAIT_MS", "");

 	if (ast_strlen_zero(data)) {
 		ast_log(LOG_WARNING, "Queue requires an argument: queuename[,options[,URL[,announceoverride[,timeout[,agi[,gosub[,rule[,position]]]]]]]]\n");
@@ -9063,6 +9121,27 @@ check_turns:
 	}

 stop:
+	if (qe.chan) {
+		ast_channel_lock(qe.chan);
+		ast_channel_stage_snapshot(qe.chan);
+		/* 1. Handle QUEUEWAIT (Total time spent waiting in queue) */
+		if (ast_strlen_zero(pbx_builtin_getvar_helper(qe.chan, "QUEUEWAIT"))) {
+			set_duration_var(qe.chan, "QUEUEWAIT", (int64_t)(time(NULL) - qe.start) * 1000);
+		}
+
+		/* 2. Handle DIALEDTIME (Total time spent from beginning of the call) */
+		if (ast_strlen_zero(pbx_builtin_getvar_helper(qe.chan, "DIALEDTIME"))) {
+			set_duration_var(qe.chan, "DIALEDTIME", ast_channel_get_duration_ms(qe.chan));
+		}
+
+		/* 3. Handle ANSWEREDTIME (Time spent talking to an agent) */
+		if (ast_strlen_zero(pbx_builtin_getvar_helper(qe.chan, "ANSWEREDTIME"))) {
+			/* If we are here and it's still empty, the call was never answered */
+			set_duration_var(qe.chan, "ANSWEREDTIME", 0);
+		}
+		ast_channel_stage_snapshot_done(qe.chan);
+		ast_channel_unlock(qe.chan);
+	}
 	if (res) {
 		if (reason == QUEUE_WITHDRAW) {
 			record_abandoned(&qe);