Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,947 changes: 2,947 additions & 0 deletions MODULE.bazel.lock

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.intuit.hooks.HookContext
import com.intuit.hooks.SyncBailHook
import com.intuit.hooks.SyncHook
import com.intuit.hooks.SyncWaterfallHook
import com.intuit.playerui.android.asset.AssetRenderException
import com.intuit.playerui.android.asset.RenderableAsset
import com.intuit.playerui.android.asset.SuspendableAsset.AsyncHydrationTrackerPlugin
import com.intuit.playerui.android.extensions.Styles
Expand All @@ -18,12 +19,15 @@ import com.intuit.playerui.android.registry.RegistryPlugin
import com.intuit.playerui.core.asset.Asset
import com.intuit.playerui.core.bridge.Completable
import com.intuit.playerui.core.bridge.format
import com.intuit.playerui.core.bridge.runtime.PlayerRuntimeConfig
import com.intuit.playerui.core.bridge.serialization.format.registerContextualSerializer
import com.intuit.playerui.core.constants.ConstantsController
import com.intuit.playerui.core.error.ErrorSeverity
import com.intuit.playerui.core.error.ErrorTypes
import com.intuit.playerui.core.experimental.ExperimentalPlayerApi
import com.intuit.playerui.core.logger.TapableLogger
import com.intuit.playerui.core.player.GetCoroutineFunction
import com.intuit.playerui.core.player.HeadlessPlayer
import com.intuit.playerui.core.player.HeadlessPlayerRuntimeConfig
import com.intuit.playerui.core.player.Player
import com.intuit.playerui.core.player.PlayerException
import com.intuit.playerui.core.player.state.CompletedState
Expand Down Expand Up @@ -65,7 +69,7 @@ public class AndroidPlayer private constructor(
public constructor(
plugins: List<Plugin>,
config: Config = Config(),
) : this(HeadlessPlayer(plugins.injectDefaultPlugins(), config = config))
) : this(HeadlessPlayer(plugins.injectDefaultPlugins(), realConfig = config))

/**
* Allow the [AndroidPlayer] to be built on top of a pre-existing
Expand Down Expand Up @@ -353,7 +357,25 @@ public class AndroidPlayer private constructor(

public data class Config(
override var debuggable: Boolean = false,
override var coroutineExceptionHandler: CoroutineExceptionHandler? = null,
// TODO: Find an alternative to changing the type here or improve the API to make a little more sense
override var coroutineExceptionHandler: GetCoroutineFunction? = { player ->
CoroutineExceptionHandler { _, throwable ->
var metadata: Map<String, Any?>? = null
if (throwable is AssetRenderException) {
metadata = mapOf(
"assetId" to throwable.rootAsset.asset.id,
)
}
player.inProgressState?.controllers?.error?.captureError(throwable, ErrorTypes.RENDER, ErrorSeverity.ERROR, metadata)
?: player.logger.error(
"Exception caught in Player scope: ${throwable.message}",
throwable.stackTrace
.joinToString("\n") {
"\tat $it"
}.replaceFirst("\tat ", "\n"),
)
}
},
override var timeout: Long = if (debuggable) Int.MAX_VALUE.toLong() else 5000,
) : PlayerRuntimeConfig()
) : HeadlessPlayerRuntimeConfig()
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,22 @@ public abstract class SuspendableAsset<Data>(
}

private suspend fun doInitView() = withContext(Dispatchers.Default) {
initView(getData()).apply { setTag(R.bool.view_hydrated, false) }
// TODO: Centralize some of this error handling so that it can be repeated easily.
try {
initView(getData()).apply { setTag(R.bool.view_hydrated, false) }
} catch (exception: Throwable) {
// ignore cancellation exceptions because those are used to rehydrate the view
if (exception is CancellationException) {
throw exception
}

if (exception is AssetRenderException) {
exception.assetParentPath += assetContext
throw exception
} else {
throw AssetRenderException(assetContext, "Failed to render asset", exception)
}
}
}

// To be launched in Dispatchers.Main
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import com.intuit.playerui.android.extensions.Styles
import com.intuit.playerui.android.extensions.into
import com.intuit.playerui.android.withContext
import com.intuit.playerui.android.withTag
import com.intuit.playerui.core.error.ErrorSeverity
import com.intuit.playerui.core.error.ErrorTypes
import com.intuit.playerui.core.experimental.ExperimentalPlayerApi
import com.intuit.playerui.core.player.state.inProgressState
import kotlinx.coroutines.launch
import kotlinx.serialization.KSerializer

Expand Down Expand Up @@ -53,7 +56,19 @@ public abstract class ComposableAsset<Data>(
@Composable
public fun compose(data: Data? = null) {
val data: Data? by produceState(initialValue = data, key1 = this) {
value = getData()
try {
value = getData()
} catch (error: Throwable) {
player.inProgressState?.controllers?.error?.captureError(
error,
ErrorTypes.RENDER,
ErrorSeverity.ERROR,
mapOf(
"assetId" to assetContext.asset.id,
),
)
null
}
}

data?.let {
Expand Down Expand Up @@ -84,6 +99,7 @@ public abstract class ComposableAsset<Data>(
) {
val assetTag = tag ?: asset.id
val containerModifier = Modifier.testTag(assetTag) then modifier
// TODO: Conditionally call withTag only if tag is provided
assetContext.withContext(LocalContext.current).withTag(assetTag).build().run {
renewHydrationScope("Creating view within a ComposableAsset")
when (this) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.intuit.playerui.android.AndroidPlayer
import com.intuit.playerui.android.AndroidPlayerPlugin
import com.intuit.playerui.android.asset.AssetRenderException
import com.intuit.playerui.android.asset.RenderableAsset
import com.intuit.playerui.core.bridge.runtime.Runtime
import com.intuit.playerui.core.error.ErrorSeverity
import com.intuit.playerui.core.error.ErrorTypes
import com.intuit.playerui.core.experimental.ExperimentalPlayerApi
import com.intuit.playerui.core.managed.AsyncFlowIterator
import com.intuit.playerui.core.managed.AsyncIterationManager
Expand Down Expand Up @@ -206,8 +209,21 @@ public open class PlayerViewModel(
}
}

public fun fail(cause: Throwable) {
player.inProgressState?.fail(cause)
public fun fail(throwable: Throwable) {
val cause = throwable.cause
// TODO: Replace type check with general exception that can have the metadata or other properties needed.
if (cause is AssetRenderException) {
player.inProgressState?.controllers?.error?.captureError(
cause,
ErrorTypes.RENDER,
ErrorSeverity.ERROR,
mapOf(
"assetId" to cause.rootAsset.asset.id,
),
)
} else {
player.inProgressState?.fail(throwable)
}
}

/** Helper to progress the [FlowManager] in within the [viewModelScope] */
Expand Down
25 changes: 24 additions & 1 deletion core/player/src/controllers/error/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@ export interface ErrorControllerOptions {
model?: DataController;
}

type ReplacerFunction = (key: string, value: any) => any;

const makeJsonStringifyReplacer = (): ReplacerFunction => {
const cache = new Set();
return (_: string, value: any) => {
if (typeof value === "object" && value !== null) {
if (cache.has(value)) {
// Circular reference found, discard key
return "[CIRCULAR]";
}
// Store value in our collection
cache.add(value);
}
return value;
};
};

/** The orchestrator for player error handling */
export class ErrorController {
public hooks: ErrorControllerHooks = {
Expand Down Expand Up @@ -88,6 +105,7 @@ export class ErrorController {
errorType,
severity,
metadata,
skipped: false,
};

// Add to history
Expand All @@ -98,14 +116,19 @@ export class ErrorController {

this.options.logger.debug(
`[ErrorController] Captured error: ${error.message}`,
{ errorType, severity, metadata },
// TODO: Find a better way to do this. Either centralize the stringify replacer in the print plugin or something else.
JSON.stringify(
{ errorType, severity, metadata },
makeJsonStringifyReplacer(),
),
);

// Notify listeners and check if navigation should be skipped
// Plugins can observe the error and optionally return true to bail
const shouldSkip = this.hooks.onError.call(playerError) ?? false;

if (shouldSkip) {
playerError.skipped = true;
this.options.logger.debug(
"[ErrorController] Error state navigation skipped by plugin",
);
Expand Down
3 changes: 3 additions & 0 deletions core/player/src/controllers/error/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const ErrorTypes = {
SCHEMA: "schema",
NETWORK: "network",
PLUGIN: "plugin",
RENDER: "render",
} as const;

/**
Expand All @@ -36,4 +37,6 @@ export interface PlayerError {
severity?: ErrorSeverity;
/** Additional metadata */
metadata?: ErrorMetadata;
/** Whether or not the error was skipped. */
skipped: boolean;
}
10 changes: 9 additions & 1 deletion core/player/src/controllers/view/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { DataController } from "../data/controller";
import type { TransformRegistry } from "./types";
import type { BindingInstance } from "../../binding";
import type { Node } from "../../view";
import { ErrorController } from "../error";

export interface ViewControllerOptions {
/** Where to get data from */
Expand All @@ -32,6 +33,9 @@ export interface ViewControllerOptions {

/** A flow-controller instance to listen for view changes */
flowController: FlowController;

/** Error controller to use when managing view-level errors */
errorController: ErrorController;
}

export type ViewControllerHooks = {
Expand Down Expand Up @@ -197,7 +201,11 @@ export class ViewController {
throw new Error(`No view with id ${viewId}`);
}

const view = new ViewInstance(source, this.viewOptions);
const view = new ViewInstance(
source,
this.viewOptions,
this.viewOptions.errorController,
);
this.currentView = view;

// Give people a chance to attach their
Expand Down
1 change: 1 addition & 0 deletions core/player/src/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ export class Player {
type: (b) => schema.getType(parseBinding(b)),
},
constants: this.constantsController,
errorController,
});

viewController.hooks.view.tap("player", (view) => {
Expand Down
Loading