PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.2.1
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.2.1
7.2.2 7.2.1 7.2 7.1.2 7.1.1 7.1 7.0.4 7.0.6 7.0.7 6.3.8 6.3.7 6.3.6 6.3.5 6.3.4 6.3.3 6.3.1 trunk 5.7.3 5.7.5 5.8.1 5.8.2 5.8.3 5.8.4 5.8.6 6.0.4 All 37 releases
← All changes | includes/mlsimport-telemetry.php +280 -60 7.0.47.2.1 View file →
@@ -35,13 +35,14 @@
35 35 // Request-scoped accumulator
36 36 // ---------------------------------------------------------------------------
37 37
38 38 /**
39 - * In-memory counter deltas for the current request.
40 - * Keys: imported | updated | deleted | syncs | token_failures.
39 + * In-memory counter deltas for the current request, nested per connection:
40 + * mls_id => (imported | updated | deleted | syncs | token_failures => delta).
41 + * mls_id 0 holds unattributed (account-level) deltas — issue #283.
41 42 * Written to wp_options exactly once — on shutdown — by mlsimport_telemetry_flush().
42 43 *
43 - * @var array<string,int>
44 + * @var array<int,array<string,int>>
44 45 */
45 46 $mlsimport_telemetry_pending = array();
46 47
47 48 // ---------------------------------------------------------------------------
@@ -52,13 +53,21 @@
52 53 * Add an in-memory counter delta for the current request.
53 54 * Allowed $metric: 'imported' | 'updated' | 'deleted' | 'syncs' | 'token_failures'.
54 55 * No DB access — deltas are written to wp_options once, on shutdown, by flush().
55 56 *
57 + * Multi-MLS (issue #283): callers pass the connection the activity belongs to
58 + * (they have it in hand from the task binding). Flush folds every delta into
59 + * the unchanged GLOBAL daily bucket AND, for a positive id, into that
60 + * connection's own bucket — so global sums stay the sum of the per-connection
61 + * buckets. mls_id 0 = account-level activity with no owning connection
62 + * (e.g. SaaS token refresh failures), counted globally only.
63 + *
56 64 * @param string $metric One of the five allowed metric keys.
57 65 * @param int $amount Amount to add (default 1).
66 + * @param int $mls_id Connection the activity belongs to (0 = unattributed).
58 67 * @return void
59 68 */
60 -function mlsimport_telemetry_bump( string $metric, int $amount = 1 ): void {
69 +function mlsimport_telemetry_bump( string $metric, int $amount = 1, int $mls_id = 0 ): void {
61 70 // Whitelist of accepted metric keys.
62 71 $allowed = array( 'imported', 'updated', 'deleted', 'syncs', 'token_failures' );
63 72 // Guard: silently ignore an unknown metric key.
64 73 if ( ! in_array( $metric, $allowed, true ) ) {
@@ -65,14 +74,16 @@
65 74 return;
66 75 }
67 76 // Reach the request-scoped accumulator.
68 77 global $mlsimport_telemetry_pending;
69 - // Lazily zero-initialise this metric's slot on first use.
70 - if ( ! isset( $mlsimport_telemetry_pending[ $metric ] ) ) {
71 - $mlsimport_telemetry_pending[ $metric ] = 0;
78 + // Normalize a negative id to the unattributed slot.
79 + $mls_id = max( 0, $mls_id );
80 + // Lazily zero-initialise this connection+metric slot on first use.
81 + if ( ! isset( $mlsimport_telemetry_pending[ $mls_id ][ $metric ] ) ) {
82 + $mlsimport_telemetry_pending[ $mls_id ][ $metric ] = 0;
72 83 }
73 84 // Add the delta (no DB touch here — flush writes on shutdown).
74 - $mlsimport_telemetry_pending[ $metric ] += $amount;
85 + $mlsimport_telemetry_pending[ $mls_id ][ $metric ] += $amount;
75 86 }
76 87
77 88 // ---------------------------------------------------------------------------
78 89 // §1 Public API — flush (registered on 'shutdown')
@@ -78,13 +89,61 @@
78 89 // §1 Public API — flush (registered on 'shutdown')
79 90 // ---------------------------------------------------------------------------
80 91
81 92 /**
82 - * Flush accumulated counter deltas into today's daily bucket.
93 + * Fold one connection's pending deltas into one daily-bucket map. Pure.
94 + *
95 + * Step by step:
96 + * 1. Zero-base today's bucket for all five counters (keeping accumulated values).
97 + * 2. Add each pending delta into its counter.
98 + * 3. Prune buckets older than the retention window.
99 + *
100 + * Shared by flush() for the GLOBAL map ('daily') and every per-connection
101 + * map ('daily_mls'[mls_id]) so both fold the same one way (issue #283).
102 + *
103 + * @param array $daily Daily bucket map (YYYY-MM-DD => counters).
104 + * @param array $pending Metric => delta for this request.
105 + * @param string $today Today's UTC date 'Y-m-d'.
106 + * @return array The updated, pruned daily map.
107 + */
108 +function mlsimport_telemetry_fold_bucket( array $daily, array $pending, string $today ): array {
109 + // Step 1: zero-base for all five counters in today's bucket.
110 + $bucket = array_merge(
111 + array(
112 + 'imported' => 0,
113 + 'updated' => 0,
114 + 'deleted' => 0,
115 + 'syncs' => 0,
116 + 'token_failures' => 0,
117 + ),
118 + isset( $daily[ $today ] ) && is_array( $daily[ $today ] ) ? $daily[ $today ] : array()
119 + );
120 +
121 + // Step 2: fold this request's deltas into the bucket.
122 + foreach ( $pending as $metric => $delta ) {
123 + if ( isset( $bucket[ $metric ] ) ) {
124 + $bucket[ $metric ] += $delta;
125 + }
126 + }
127 +
128 + // Step 3: store the bucket, drop buckets past the retention window.
129 + $daily[ $today ] = $bucket;
130 + return mlsimport_telemetry_prune_buckets( $daily, $today );
131 +}
132 +
133 +/**
134 + * Flush accumulated counter deltas into today's daily buckets.
83 135 * No-op when nothing is pending. Reads + writes the single option
84 136 * 'mlsimport_telemetry_state' exactly once, prunes buckets older than 8 days,
85 137 * resets the pending array. Registered on the 'shutdown' action.
86 138 *
139 + * Multi-MLS (issue #283): pending deltas arrive nested per connection.
140 + * Every delta folds into the unchanged GLOBAL 'daily' map; a positive
141 + * connection id additionally folds into that connection's own map under
142 + * 'daily_mls' — so the global 7-day sums equal the sum of the per-connection
143 + * buckets by construction. A connection whose fold carried import activity
144 + * (imported/updated/deleted) also gets its 'connection_last_import' stamp.
145 + *
87 146 * @return void
88 147 */
89 148 function mlsimport_telemetry_flush(): void {
90 149 // Reach the request-scoped accumulator.
@@ -100,39 +159,44 @@
100 159 if ( ! is_array( $state ) ) {
101 160 $state = array();
102 161 }
103 162
104 - // Ensure the daily bucket map exists.
163 + // Ensure the global and per-connection bucket maps exist.
105 164 if ( ! isset( $state['daily'] ) || ! is_array( $state['daily'] ) ) {
106 165 $state['daily'] = array();
107 166 }
167 + if ( ! isset( $state['daily_mls'] ) || ! is_array( $state['daily_mls'] ) ) {
168 + $state['daily_mls'] = array();
169 + }
108 170
109 - // Today's UTC date is the bucket key; start from any existing bucket.
110 - $today = gmdate( 'Y-m-d' );
111 - $bucket = isset( $state['daily'][ $today ] ) ? $state['daily'][ $today ] : array();
171 + // Today's UTC date is the bucket key everywhere.
172 + $today = gmdate( 'Y-m-d' );
112 173
113 - // Initialise zero-base for all five counters in this bucket.
114 - $defaults = array(
115 - 'imported' => 0,
116 - 'updated' => 0,
117 - 'deleted' => 0,
118 - 'syncs' => 0,
119 - 'token_failures' => 0,
120 - );
121 - // Fill any missing counters with 0 while keeping already-accumulated values.
122 - $bucket = array_merge( $defaults, $bucket );
174 + // Fold every connection's deltas — each connection once, globals once each.
175 + foreach ( $mlsimport_telemetry_pending as $mls_id => $pending ) {
176 + // Every delta counts globally (legacy fields unchanged).
177 + $state['daily'] = mlsimport_telemetry_fold_bucket( $state['daily'], $pending, $today );
123 178
124 - // Fold this request's pending deltas into today's bucket.
125 - foreach ( $mlsimport_telemetry_pending as $metric => $delta ) {
126 - if ( isset( $bucket[ $metric ] ) ) {
127 - $bucket[ $metric ] += $delta;
179 + // Unattributed (account-level) deltas stop at the global map.
180 + if ( $mls_id <= 0 ) {
181 + continue;
128 182 }
183 +
184 + // This connection's own bucket map.
185 + $mls_daily = is_array( $state['daily_mls'][ $mls_id ] ?? null ) ? $state['daily_mls'][ $mls_id ] : array();
186 + $state['daily_mls'][ $mls_id ] = mlsimport_telemetry_fold_bucket( $mls_daily, $pending, $today );
187 +
188 + // Import activity stamps this connection's last-import time (#283) —
189 + // syncs/token ticks alone are not imports and do not move it.
190 + $activity = (int) ( $pending['imported'] ?? 0 ) + (int) ( $pending['updated'] ?? 0 ) + (int) ( $pending['deleted'] ?? 0 );
191 + if ( $activity > 0 ) {
192 + if ( ! isset( $state['connection_last_import'] ) || ! is_array( $state['connection_last_import'] ) ) {
193 + $state['connection_last_import'] = array();
194 + }
195 + $state['connection_last_import'][ $mls_id ] = time();
196 + }
129 197 }
130 198
131 - // Store the updated bucket, then drop buckets older than the retention window.
132 - $state['daily'][ $today ] = $bucket;
133 - $state['daily'] = mlsimport_telemetry_prune_buckets( $state['daily'], $today );
134 -
135 199 // Single write, non-autoloaded.
136 200 update_option( 'mlsimport_telemetry_state', $state, false );
137 201
138 202 // Reset pending.
@@ -191,8 +255,86 @@
191 255 update_option( 'mlsimport_telemetry_state', $state, false );
192 256 }
193 257
194 258 /**
259 + * Record the outcome of one listings request into sync-health telemetry
260 + * (GitHub issue #207).
261 + *
262 + * Called from the single choke point every import path routes through
263 + * (Mlsimport_Admin::mlsimport_make_listing_requests()), with the already
264 + * normalized API answer. Stamps last_sync_success when the pull returned a
265 + * feed, so a cron run that dies later in its loop still leaves fresh
266 + * success evidence — the previous end-of-loop-only stamp left actively
267 + * syncing sites reporting last_successful_sync = "never".
268 + *
269 + * Multi-MLS (issue #283): the caller passes the connection the pull ran for
270 + * (in hand from the task binding). Each pull ticks that connection's 'syncs'
271 + * counter, and the outcome is additionally stamped into the per-connection
272 + * success/failure maps — the GLOBAL sync_health stamps stay exactly as before.
273 + *
274 + * @param mixed $answer The normalized listings API answer array.
275 + * @param int $mls_id Connection the pull ran for (0 = unattributed).
276 + * @return void
277 + */
278 +function mlsimport_telemetry_record_sync_result( $answer, int $mls_id = 0 ): void {
279 + // One pull = one sync tick, counted against its own connection (#283).
280 + // This is also what makes syncs_last_7_days a live counter again.
281 + mlsimport_telemetry_bump( 'syncs', 1, $mls_id );
282 +
283 + // A successful pull always carries the feed count under 'results'.
284 + if ( is_array( $answer ) && isset( $answer['results'] ) ) {
285 + mlsimport_telemetry_set( 'last_sync_success', time() );
286 + // Per-connection success stamp (#283).
287 + if ( $mls_id > 0 ) {
288 + mlsimport_telemetry_record_connection_sync( $mls_id, true );
289 + }
290 + return;
291 + }
292 + // Anything else is a failed pull: stamp when it happened and a real
293 + // failure class — previously every failure surfaced as "unknown".
294 + $code = mlsimport_telemetry_classify_sync_failure( $answer );
295 + mlsimport_telemetry_set( 'last_sync_failed', time() );
296 + mlsimport_telemetry_set( 'last_sync_failed_code', $code );
297 + // Per-connection failure stamp (#283).
298 + if ( $mls_id > 0 ) {
299 + mlsimport_telemetry_record_connection_sync( $mls_id, false, $code );
300 + }
301 +}
302 +
303 +/**
304 + * Map a failed listings answer to a short failure class for
305 + * sync_health.last_failure_code. Pure — inspects only the answer shape and
306 + * the message strings globalApiRequestCurlSaas() actually produces.
307 + *
308 + * @param mixed $answer The normalized failed listings API answer.
309 + * @return string One of the short failure-class codes.
310 + */
311 +function mlsimport_telemetry_classify_sync_failure( $answer ): string {
312 + // A provider-rule rejection already carries a machine code under 'type'
313 + // (set by mlsimport_make_listing_requests()) — pass it through as-is.
314 + if ( is_array( $answer ) && ! empty( $answer['type'] ) ) {
315 + return (string) $answer['type'];
316 + }
317 + $message = is_array( $answer ) && isset( $answer['message'] ) ? (string) $answer['message'] : '';
318 + // The exact string ThemeImport returns when the SaaS JWT cannot be
319 + // minted/refreshed (bad account credentials, token endpoint down).
320 + if ( 'Token validation failed' === $message ) {
321 + return 'token';
322 + }
323 + // WP_Error transport messages pass through verbatim; cURL timeouts read
324 + // 'cURL error 28: Operation timed out after N milliseconds ...'.
325 + if ( false !== stripos( $message, 'timed out' ) ) {
326 + return 'timeout';
327 + }
328 + // AWS API Gateway rejections decode to {"message":"Unauthorized"} /
329 + // {"message":"Forbidden"} with no 'results' key.
330 + if ( false !== stripos( $message, 'unauthorized' ) || false !== stripos( $message, 'forbidden' ) ) {
331 + return 'auth';
332 + }
333 + return 'api_error';
334 +}
335 +
336 +/**
195 337 * Record the first-completion time of an onboarding-wizard step into the
196 338 * 'onboarding_steps' map on mlsimport_telemetry_state. First completion wins;
197 339 * re-running a step does not move the timestamp. Saved with autoload = 'no'.
198 340 *
@@ -222,8 +364,52 @@
222 364 update_option( 'mlsimport_telemetry_state', $state, false );
223 365 }
224 366
225 367 // ---------------------------------------------------------------------------
368 +// §1 Import performance snapshot (GitHub issue #216)
369 +// ---------------------------------------------------------------------------
370 +
371 +/**
372 + * Build the import-performance snapshot for one finished Import Run. Pure.
373 + *
374 + * Answers support's "is it us or the host?" question from data the run
375 + * machinery already tracks:
376 + * - elapsed_seconds: wall time from the run's started_at to its finish, across
377 + * every chunk worker — not just the finishing request.
378 + * - workers: 1 + chunk hand-offs + watchdog revivals. Any revival means a
379 + * worker died without handing off, i.e. the host killed it.
380 + * - queue_depth: pending worker actions at finish — backlog evidence.
381 + * - peak_memory_mb: peak PHP memory of the finishing worker.
382 + *
383 + * @param array $run Final Import Run record (started_at, source,
384 + * expected, handoffs, revive_count).
385 + * @param array $result Final public Import Run Result.
386 + * @param int $now Finish time (Unix epoch).
387 + * @param int $peak_memory_bytes memory_get_peak_usage(true) of the finisher.
388 + * @param int $queue_depth Pending worker actions for the import hook.
389 + * @return array<string,int|string> The snapshot stored under 'last_import_run'.
390 + */
391 +function mlsimport_telemetry_import_run_snapshot( array $run, array $result, int $now, int $peak_memory_bytes, int $queue_depth ): array {
392 + // Wall time across the whole worker chain; guard against a missing or
393 + // future started_at leaving a negative duration.
394 + $started_at = (int) ( $run['started_at'] ?? $now );
395 + return array(
396 + 'source' => (string) ( $run['source'] ?? '' ),
397 + 'state' => (string) ( $result['state'] ?? '' ),
398 + 'expected' => (int) ( $run['expected'] ?? 0 ),
399 + 'saved' => (int) ( $result['saved'] ?? 0 ),
400 + 'failed' => (int) ( $result['failed'] ?? 0 ),
401 + 'elapsed_seconds' => max( 0, $now - $started_at ),
402 + // One initial worker, plus one per chunk hand-off, plus one per
403 + // watchdog revival (a revival is a worker the host killed).
404 + 'workers' => 1 + (int) ( $run['handoffs'] ?? 0 ) + (int) ( $run['revive_count'] ?? 0 ),
405 + 'peak_memory_mb' => (int) round( $peak_memory_bytes / 1048576 ),
406 + 'queue_depth' => $queue_depth,
407 + 'finished_at' => $now,
408 + );
409 +}
410 +
411 +// ---------------------------------------------------------------------------
226 412 // §1 Pure helpers
227 413 // ---------------------------------------------------------------------------
228 414
229 415 /**
@@ -411,13 +597,16 @@
411 597 'orderby' => 'date',
412 598 'order' => 'DESC',
413 599 'meta_query' => array(
414 600 array(
415 - 'key' => 'ListingKey',
601 + 'key' => '_mlsimport_listing_key',
416 602 'compare' => 'EXISTS',
417 603 ),
418 604 ),
419 605 'no_found_rows' => true,
606 + // Telemetry samples STORED listings; dedupe-hidden copies (#282) are
607 + // stored and must count.
608 + 'mlsimport_include_hidden' => true,
420 609 );
421 610
422 611 // Run the query (guard for environments without get_posts()).
423 612 $post_ids = function_exists( 'get_posts' ) ? get_posts( $args ) : array();
@@ -550,8 +739,17 @@
550 739
551 740 // --- data completeness ---
552 741 $completeness = mlsimport_telemetry_sample_completeness();
553 742
743 + // --- import performance (issue #216) ---
744 + // The latest finished-run snapshot, recorded at finish_run(). Null means
745 + // no run has ever finished on this install — distinct from a missing field.
746 + $import_performance = null;
747 + if ( isset( $state['last_import_run'] ) && is_array( $state['last_import_run'] ) ) {
748 + $import_performance = $state['last_import_run'];
749 + $import_performance['finished_at'] = mlsimport_telemetry_iso( (int) ( $import_performance['finished_at'] ?? 0 ) );
750 + }
751 +
554 752 // --- configuration: import tasks ---
555 753 $raw_tasks_query = function_exists( 'get_posts' ) ? get_posts( array(
556 754 'post_type' => 'mlsimport_item',
557 755 'post_status' => 'any',
@@ -559,10 +757,16 @@
559 757 'fields' => 'ids',
560 758 'no_found_rows' => true,
561 759 ) ) : array();
562 760
563 - $import_tasks = array();
564 - $auto_update_any = false;
761 + // --- connections registry (issue #283) ---
762 + // One record per registered MLS, priority-sorted (1 first). The
763 + // class_exists guard mirrors the get_posts/WP_Query guards above: legacy
764 + // unit harnesses load this file without the registry class.
765 + $connection_records = class_exists( 'Mlsimport_Connections' ) ? Mlsimport_Connections::all() : array();
766 +
767 + $import_tasks = array();
768 + $auto_update_any = false;
565 769 foreach ( $raw_tasks_query as $task_id ) {
566 770 $how_many = (int) get_post_meta( $task_id, 'mlsimport_item_how_many', true );
567 771 $stat_cron = (int) get_post_meta( $task_id, 'mlsimport_item_stat_cron', true );
568 772 $auto_upd = ( 1 === $stat_cron );
@@ -574,24 +778,41 @@
574 778 'auto_update' => $auto_upd,
575 779 );
576 780 }
577 781
782 + // Per-connection workload (issue #283): task/paused/listing counts per
783 + // connection, gathered by the module that owns the per-connection half
784 + // of the heartbeat. Skipped entirely on an empty registry (also keeps
785 + // legacy unit harnesses off the binding-module functions).
786 + $connection_workload = $connection_records
787 + ? mlsimport_telemetry_gather_connection_workload( $connection_records, $raw_tasks_query, $post_type )
788 + : array();
789 +
578 790 // --- MLS provider / ID ---
791 + // Legacy singular fields (decision #272): filled from the PRIORITY-1
792 + // connection so the current portal keeps working while it learns the
793 + // connections array. An empty registry keeps the pre-multi-MLS derivation.
579 794 $mls_provider = '';
580 795 $mls_id = 0;
581 - if ( isset( $opts['mlsimport_mls_name'] ) && '' !== $opts['mlsimport_mls_name'] ) {
582 - $mls_id = (int) $opts['mlsimport_mls_name'];
796 + if ( $connection_records ) {
797 + $priority_one = reset( $connection_records );
798 + $mls_id = (int) $priority_one['mls_id'];
799 + $mls_provider = (string) $priority_one['provider_type'];
800 + } else {
801 + if ( isset( $opts['mlsimport_mls_name'] ) && '' !== $opts['mlsimport_mls_name'] ) {
802 + $mls_id = (int) $opts['mlsimport_mls_name'];
803 + }
804 + // Derive MLS provider label from the theme/MLS env class name if available.
805 + if (
806 + isset( $mlsimport ) &&
807 + isset( $mlsimport->admin ) &&
808 + isset( $mlsimport->admin->mls_env_data ) &&
809 + is_object( $mlsimport->admin->mls_env_data )
810 + ) {
811 + $mls_class = get_class( $mlsimport->admin->mls_env_data );
812 + $mls_provider = ( 'stdClass' !== $mls_class ) ? $mls_class : '';
813 + }
583 814 }
584 - // Derive MLS provider label from the theme/MLS env class name if available.
585 - if (
586 - isset( $mlsimport ) &&
587 - isset( $mlsimport->admin ) &&
588 - isset( $mlsimport->admin->mls_env_data ) &&
589 - is_object( $mlsimport->admin->mls_env_data )
590 - ) {
591 - $mls_class = get_class( $mlsimport->admin->mls_env_data );
592 - $mls_provider = ( 'stdClass' !== $mls_class ) ? $mls_class : '';
593 - }
594 815
595 816 // Theme label.
596 817 $theme_label = '';
597 818 if (
@@ -645,8 +866,9 @@
645 866 'with_address_percent' => (int) $completeness['with_address_percent'],
646 867 'with_coordinates_percent' => (int) $completeness['with_coordinates_percent'],
647 868 ),
648 869 ),
870 + 'import_performance' => $import_performance,
649 871 'engagement' => array(
650 872 'last_admin_page_view' => mlsimport_telemetry_iso( $last_admin_load ),
651 873 'last_import_task_page_view' => mlsimport_telemetry_iso( $last_import_task_load ),
652 874 ),
@@ -656,8 +878,17 @@
656 878 'import_tasks' => $import_tasks,
657 879 'import_tasks_count' => count( $import_tasks ),
658 880 'auto_update_enabled' => (bool) $auto_update_any,
659 881 ),
882 + // Per-connection health (issue #283, decision #272): one entry per
883 + // registered connection, priority order; a single-connection install
884 + // sends the identical shape with a one-entry array.
885 + 'connections' => mlsimport_telemetry_connections_payload(
886 + $connection_records,
887 + $state,
888 + $today,
889 + $connection_workload
890 + ),
660 891 'environment' => array(
661 892 'plugin_version' => defined( 'MLSIMPORT_VERSION' ) ? MLSIMPORT_VERSION : '',
662 893 'php_version' => PHP_VERSION,
663 894 'wordpress_version' => get_bloginfo( 'version' ),
@@ -806,18 +1037,7 @@
806 1037 $state['last_field_management'] = $now;
807 1038 update_option( 'mlsimport_telemetry_state', $state, false );
808 1039 }
809 1040
810 -// Field-selector progressive-save AJAX actions — "managing import fields".
811 -// Priority 1 so the timestamp is recorded before the real save handler runs.
812 -foreach (
813 - array(
814 - 'mlsimport_save_field_chunk',
815 - 'mlsimport_save_field_option',
816 - 'mlsimport_save_field_position',
817 - 'mlsimport_save_bulk_import',
818 - 'mlsimport_save_bulk_admin',
819 - ) as $mlsimport_field_action
820 -) {
821 - add_action( 'wp_ajax_' . $mlsimport_field_action, 'mlsimport_telemetry_track_field_management', 1 );
822 -}
823 -unset( $mlsimport_field_action );
1041 +// The single compact mutation endpoint is the Field Configuration activity
1042 +// seam. Priority 1 records activity before validation/persistence runs.
1043 +add_action( 'wp_ajax_mlsimport_change_field_configuration', 'mlsimport_telemetry_track_field_management', 1 );