diff --git a/README.md b/README.md index 2aaca34..dfade1d 100644 --- a/README.md +++ b/README.md @@ -1000,6 +1000,34 @@ provider (the payload is the project's data) and when the function was not called. There is no default Code integration: every call names the integration it means (`IntegrationId`), as the examples above do. +## Marketplace function runs + +Each call of a marketplace function is one run row: an invoke (user, API +key, AI), a trigger run or a scheduler fire. +`Code.GetMarketplaceFunctionRunsAsync` lists them — set exactly one of +`FunctionId` (every source, newest first, up to 50), `TriggerId` or +`TaskId`. A row carries `Status` (`ok` / `failed` / `skipped`, `null` on +rows written before the field existed), `DurationMs`, `Source`, `Event`, +`CorrelationId` and `HasDetails`. + +The input, steps and response of a run are written to the project log under +the row's `CorrelationId`, only while the environment's Norbix Logging is on +— then `HasDetails` is `true`. `GetMarketplaceFunctionRunDetailsAsync` (in +`Norbix.Hub`, a hand-written helper over `Logs.GetLogsByCorrelationIdAsync`, +`GET /{version}/logs/audit?targetCorrelationId=…`) reads them: + +```csharp +var runs = await hub.Code.GetMarketplaceFunctionRunsAsync( + new GetMarketplaceFunctionRuns { FunctionId = "func_…", Limit = 10 }); + +foreach (var run in runs?.Runs ?? []) +{ + if (!run.HasDetails || run.CorrelationId is null) continue; // logs were off + var details = await hub.GetMarketplaceFunctionRunDetailsAsync(run.CorrelationId); + // details.Items: marketplace:run:started → marketplace:run:step … → completed | failed | skipped +} +``` + ## Coverage Notes Generated coverage tracks the API and Hub DTO contract files. Some flows are not generated automatically: diff --git a/src/Norbix.Hub/NorbixHubClient.Convenience.cs b/src/Norbix.Hub/NorbixHubClient.Convenience.cs index e74be0b..8d094d9 100644 --- a/src/Norbix.Hub/NorbixHubClient.Convenience.cs +++ b/src/Norbix.Hub/NorbixHubClient.Convenience.cs @@ -23,5 +23,25 @@ public sealed partial class NorbixHubClient }, cancellationToken); } -} + /// + /// The details of one marketplace function run: the project log lines filed + /// under the run's correlation id (marketplace:run:started, step, + /// then completed, failed or skipped). Pass + /// of a row from + /// Code.GetMarketplaceFunctionRunsAsync. Only a row with + /// has lines — they are + /// written only while the environment's Norbix Logging is on. + /// Same call as Logs.GetLogsByCorrelationIdAsync + /// (GET /{version}/logs/audit?targetCorrelationId=…). + /// + public Task GetMarketplaceFunctionRunDetailsAsync( + string correlationId, + CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(correlationId); + return Logs.GetLogsByCorrelationIdAsync( + new GetLogsByCorrelationId { TargetCorrelationId = correlationId }, + cancellationToken); + } +} diff --git a/src/Norbix.Sdk.Types/Generated/Hub.dtos.cs b/src/Norbix.Sdk.Types/Generated/Hub.dtos.cs index f9027d3..0e921cc 100644 --- a/src/Norbix.Sdk.Types/Generated/Hub.dtos.cs +++ b/src/Norbix.Sdk.Types/Generated/Hub.dtos.cs @@ -3901,6 +3901,9 @@ public partial class MarketplaceFunctionRunDto [DataMember] public virtual bool Succeeded { get; set; } + [DataMember] + public virtual string? Status { get; set; } + [DataMember] public virtual long? DurationMs { get; set; } @@ -3910,6 +3913,18 @@ public partial class MarketplaceFunctionRunDto [DataMember] public virtual string? VendorRequestId { get; set; } + [DataMember] + public virtual string? CorrelationId { get; set; } + + [DataMember] + public virtual bool HasDetails { get; set; } + + [DataMember] + public virtual string? TriggerId { get; set; } + + [DataMember] + public virtual string? TaskId { get; set; } + [DataMember] public virtual string? Input { get; set; } } @@ -11422,12 +11437,17 @@ public partial class GetMarketplaceFunctionRuns : CodeMashRequestBase, INorbixRequest { /// - ///The trigger whose marketplace function runs to list (trg_…). One of triggerId / taskId. + ///The marketplace function whose runs to list, from every source (func_…). One of functionId / triggerId / taskId. + /// + public virtual string? FunctionId { get; set; } + + /// + ///The trigger whose marketplace function runs to list (trg_…). One of functionId / triggerId / taskId. /// public virtual string? TriggerId { get; set; } /// - ///The scheduler task whose marketplace function runs to list (tsk_…). One of triggerId / taskId. + ///The scheduler task whose marketplace function runs to list (tsk_…). One of functionId / triggerId / taskId. /// public virtual string? TaskId { get; set; } diff --git a/tests/Norbix.Hub.Tests/MarketplaceRunsTests.cs b/tests/Norbix.Hub.Tests/MarketplaceRunsTests.cs new file mode 100644 index 0000000..b2239ec --- /dev/null +++ b/tests/Norbix.Hub.Tests/MarketplaceRunsTests.cs @@ -0,0 +1,87 @@ +using Norbix.Sdk; +using Norbix.Sdk.Tests.Helpers; +using Norbix.Sdk.Types.Hub; + +using NUnit.Framework; + +using VerifyNUnit; + +namespace Norbix.Hub.Tests; + +/// +/// GET /{version}/code/marketplace/functions/runs takes exactly one of +/// functionId / triggerId / taskId. The coverage snapshot fills every field at +/// once; these cases pin a function's own runs (functionId alone, on the query, +/// no body) and the details helper over GET /{version}/logs/audit. +/// Nothing leaves the process. +/// +[TestFixture] +public sealed class MarketplaceRunsTests +{ + private const string CorrelationId = "0f8e2c9a4b7d4e1f9a3c5b6d7e8f9012"; + + private static NorbixTestFixture NewFixture() => + NorbixTestFixture.Create(o => + { + o.AccountId = "test-account"; + o.BearerToken = "test-bearer"; + }); + + [Test] + public async Task Function_runs_send_functionId_and_limit_on_the_query_with_no_body() + { + using var fixture = NewFixture(); + fixture.RespondNext(new + { + runs = new[] + { + new + { + functionId = "func_1p4R3gTobGZpD6fXvHT4rd", + source = "api-key", + @event = "invoke", + succeeded = true, + status = "ok", + correlationId = CorrelationId, + hasDetails = true, + }, + }, + }); + + var response = await fixture.Client.Code.GetMarketplaceFunctionRunsAsync( + new GetMarketplaceFunctionRuns { FunctionId = "func_1p4R3gTobGZpD6fXvHT4rd", Limit = 50 }); + + // The snapshot pins verb, path, the exact query (functionId + limit only) and no body, + // and the new row fields read back from the generated response. + await Verifier.Verify(new { Sent = fixture.LastRequest, response!.Runs }, VerifyConfig.VerifySettings); + } + + [Test] + public async Task Run_details_read_the_log_lines_by_targetCorrelationId() + { + using var fixture = NewFixture(); + + await fixture.Client.GetMarketplaceFunctionRunDetailsAsync(CorrelationId); + + // GET /v2/logs/audit?targetCorrelationId=… — the DTO property name, not "correlationId". + await Verifier.Verify(fixture.LastRequest, VerifyConfig.VerifySettings); + } + + [Test] + public async Task Run_details_refuse_an_empty_correlation_id() + { + using var fixture = NewFixture(); + + string? error = null; + try + { + await fixture.Client.GetMarketplaceFunctionRunDetailsAsync(" "); + } + catch (ArgumentException ex) + { + error = $"{ex.GetType().Name}: {ex.ParamName}"; + } + + await Verifier.Verify(new { Error = error, NothingSent = fixture.RecordedRequests.Count == 0 }, VerifyConfig.VerifySettings); + } +} diff --git a/tests/Norbix.Hub.Tests/test_results/EndpointCoverageTests.Hub.Code.verified.txt b/tests/Norbix.Hub.Tests/test_results/EndpointCoverageTests.Hub.Code.verified.txt index 9fd9f75..d18ee68 100644 --- a/tests/Norbix.Hub.Tests/test_results/EndpointCoverageTests.Hub.Code.verified.txt +++ b/tests/Norbix.Hub.Tests/test_results/EndpointCoverageTests.Hub.Code.verified.txt @@ -700,7 +700,7 @@ Sent: { Method: GET, Path: /v2/code/marketplace/functions/runs, - Query: ?triggerId={guid}&taskId={guid}&limit=1&env=test-env, + Query: ?functionId={guid}&triggerId={guid}&taskId={guid}&limit=1&env=test-env, Headers: { Authorization: Bearer test-bearer, norbix-account-id: test-account, @@ -711,7 +711,8 @@ Response: { Runs: [ { - Succeeded: false + Succeeded: false, + HasDetails: false } ], ResponseStatus: { diff --git a/tests/Norbix.Hub.Tests/test_results/MarketplaceRunsTests.Function_runs_send_functionId_and_limit_on_the_query_with_no_body.verified.txt b/tests/Norbix.Hub.Tests/test_results/MarketplaceRunsTests.Function_runs_send_functionId_and_limit_on_the_query_with_no_body.verified.txt new file mode 100644 index 0000000..14d4f35 --- /dev/null +++ b/tests/Norbix.Hub.Tests/test_results/MarketplaceRunsTests.Function_runs_send_functionId_and_limit_on_the_query_with_no_body.verified.txt @@ -0,0 +1,23 @@ +{ + Sent: { + Method: GET, + Path: /v2/code/marketplace/functions/runs, + Query: ?functionId=func_1p4R3gTobGZpD6fXvHT4rd&limit=50, + Headers: { + Authorization: Bearer test-bearer, + norbix-account-id: test-account, + norbix-project-id: test-project + } + }, + Runs: [ + { + Event: invoke, + Source: api-key, + FunctionId: func_1p4R3gTobGZpD6fXvHT4rd, + Succeeded: true, + Status: ok, + CorrelationId: Guid_1, + HasDetails: true + } + ] +} \ No newline at end of file diff --git a/tests/Norbix.Hub.Tests/test_results/MarketplaceRunsTests.Run_details_read_the_log_lines_by_targetCorrelationId.verified.txt b/tests/Norbix.Hub.Tests/test_results/MarketplaceRunsTests.Run_details_read_the_log_lines_by_targetCorrelationId.verified.txt new file mode 100644 index 0000000..cf664ce --- /dev/null +++ b/tests/Norbix.Hub.Tests/test_results/MarketplaceRunsTests.Run_details_read_the_log_lines_by_targetCorrelationId.verified.txt @@ -0,0 +1,10 @@ +{ + Method: GET, + Path: /v2/logs/audit, + Query: ?targetCorrelationId=0f8e2c9a4b7d4e1f9a3c5b6d7e8f9012, + Headers: { + Authorization: Bearer test-bearer, + norbix-account-id: test-account, + norbix-project-id: test-project + } +} \ No newline at end of file diff --git a/tests/Norbix.Hub.Tests/test_results/MarketplaceRunsTests.Run_details_refuse_an_empty_correlation_id.verified.txt b/tests/Norbix.Hub.Tests/test_results/MarketplaceRunsTests.Run_details_refuse_an_empty_correlation_id.verified.txt new file mode 100644 index 0000000..4489feb --- /dev/null +++ b/tests/Norbix.Hub.Tests/test_results/MarketplaceRunsTests.Run_details_refuse_an_empty_correlation_id.verified.txt @@ -0,0 +1,4 @@ +{ + Error: ArgumentException: correlationId, + NothingSent: true +} \ No newline at end of file