Skip to content

Conversation

@runningcode
Copy link
Contributor

@runningcode runningcode commented Nov 26, 2025

Summary

Adds backend support for capturing build timestamps for Android artifacts via the assemble endpoint. This allows sentry-cli to pass date_built when uploading APK/AAB files.

Unlike iOS, Android artifacts don't contain reliable build timestamps due to reproducible builds, so the timestamp must be captured at build time by the Gradle plugin and passed through sentry-cli. See discussion in EME-631 Android date_built

Changes

  • API Schema: Add date_built field to assemble endpoint schema validation
  • Task Function: Parse ISO 8601 date strings to datetime objects and store in database
  • Database: Uses existing PreprodArtifact.date_built field (no migration needed)
  • Tests: Add schema validation tests and integration test for artifact creation

Related

…EME-631)

Add support for capturing build timestamps for Android artifacts via the
assemble endpoint. This allows sentry-cli to pass date_built when uploading
APK/AAB files, since Android artifacts don't contain reliable build
timestamps (due to reproducible builds).

Changes:
- Add date_built to assemble endpoint schema validation
- Parse ISO 8601 date strings to datetime objects
- Store date_built in PreprodArtifact model (field already exists)
- Add tests for schema validation and artifact creation
@linear
Copy link

linear bot commented Nov 26, 2025

@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Nov 26, 2025
"chunks": "The chunks field is required and must be provided as an array of 40-character hexadecimal strings.",
"build_configuration": "The build_configuration field must be a string.",
"release_notes": "The release_notes field msut be a string.",
"date_built": "The date_built field must be an ISO 8601 formatted date-time string.",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured with ISO-8601 we can set and send the local time zone information from the gradle plugin.

# Set date_built if provided and artifact was just created
if created and parsed_date_built:
preprod_artifact.date_built = parsed_date_built
preprod_artifact.save(update_fields=["date_built"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: date_built not updated for existing artifacts

The date_built field is only set when a new artifact is created (created == True), but not when an existing artifact is retrieved by get_or_create. If the same artifact is uploaded multiple times (e.g., retries, CI reruns), subsequent uploads with a different or corrected date_built timestamp will silently ignore the new value, leaving the original timestamp unchanged. This breaks the intended functionality of capturing accurate build timestamps.

Fix in Cursor Fix in Web

logger.warning(
"Failed to parse date_built",
extra={"date_built": date_built, "error": str(e)},
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Invalid date_built strings silently ignored after validation

The schema accepts any string for date_built without validating ISO 8601 format. Invalid date strings like "invalid-date" pass schema validation but fail during datetime.fromisoformat() parsing, causing the error to be logged as a warning while the artifact is created successfully with date_built set to null. Users receive no error response indicating their timestamp was invalid and discarded, leading to silent data loss.

Additional Locations (1)

Fix in Cursor Fix in Web

@codecov
Copy link

codecov bot commented Nov 26, 2025

❌ 4 Tests Failed:

Tests completed Failed Passed Skipped
30053 4 30049 240
View the top 3 failed test(s) by shortest run time
tests.sentry.preprod.api.endpoints.test_organization_preprod_artifact_assemble.ProjectPreprodArtifactAssembleTest::test_assemble_create_artifact_failure
Stack Traces | 2.28s run time
#x1B[1m#x1B[.../api/endpoints/test_organization_preprod_artifact_assemble.py#x1B[0m:936: in test_assemble_create_artifact_failure
    mock_create_preprod_artifact.assert_called_once_with(
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1....../x64/lib/python3.13/unittest/mock.py#x1B[0m:989: in assert_called_once_with
    return self.assert_called_with(*args, **kwargs)
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1....../x64/lib/python3.13/unittest/mock.py#x1B[0m:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
#x1B[1m#x1B[31mE   AssertionError: expected call not found.#x1B[0m
#x1B[1m#x1B[31mE   Expected: create_preprod_artifact(org_id=4557180649340928, project_id=4557180649406464, checksum='6b258c37d65ee20d22942d85c0b660600db4062b', build_configuration_name=None, release_notes=None, head_sha=None, base_sha=None, provider=None, head_repo_name=None, base_repo_name=None, head_ref=None, base_ref=None, pr_number=None)#x1B[0m
#x1B[1m#x1B[31mE     Actual: create_preprod_artifact(org_id=4557180649340928, project_id=4557180649406464, checksum='6b258c37d65ee20d22942d85c0b660600db4062b', build_configuration_name=None, release_notes=None, date_built=None, head_sha=None, base_sha=None, provider=None, head_repo_name=None, base_repo_name=None, head_ref=None, base_ref=None, pr_number=None)#x1B[0m
tests.sentry.preprod.api.endpoints.test_organization_preprod_artifact_assemble.ProjectPreprodArtifactAssembleTest::test_assemble_with_metadata
Stack Traces | 2.6s run time
#x1B[1m#x1B[.../api/endpoints/test_organization_preprod_artifact_assemble.py#x1B[0m:650: in test_assemble_with_metadata
    mock_create_preprod_artifact.assert_called_once_with(
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1....../x64/lib/python3.13/unittest/mock.py#x1B[0m:989: in assert_called_once_with
    return self.assert_called_with(*args, **kwargs)
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1....../x64/lib/python3.13/unittest/mock.py#x1B[0m:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
#x1B[1m#x1B[31mE   AssertionError: expected call not found.#x1B[0m
#x1B[1m#x1B[31mE   Expected: create_preprod_artifact(org_id=4557180653338624, project_id=4557180653338624, checksum='4f642d335a18fd05138680b41888d45b61350fdd', build_configuration_name='release', release_notes=None, head_sha='eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', base_sha='ffffffffffffffffffffffffffffffffffffffff', provider='github', head_repo_name='owner/repo', base_repo_name='owner/repo', head_ref='feature/xyz', base_ref='main', pr_number=123)#x1B[0m
#x1B[1m#x1B[31mE     Actual: create_preprod_artifact(org_id=4557180653338624, project_id=4557180653338624, checksum='4f642d335a18fd05138680b41888d45b61350fdd', build_configuration_name='release', release_notes=None, date_built=None, head_sha='eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', base_sha='ffffffffffffffffffffffffffffffffffffffff', provider='github', head_repo_name='owner/repo', base_repo_name='owner/repo', head_ref='feature/xyz', base_ref='main', pr_number=123)#x1B[0m
tests.sentry.preprod.api.endpoints.test_organization_preprod_artifact_assemble.ProjectPreprodArtifactAssembleTest::test_assemble_basic
Stack Traces | 2.73s run time
#x1B[1m#x1B[.../api/endpoints/test_organization_preprod_artifact_assemble.py#x1B[0m:574: in test_assemble_basic
    mock_create_preprod_artifact.assert_called_once_with(
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1....../x64/lib/python3.13/unittest/mock.py#x1B[0m:989: in assert_called_once_with
    return self.assert_called_with(*args, **kwargs)
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1....../x64/lib/python3.13/unittest/mock.py#x1B[0m:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
#x1B[1m#x1B[31mE   AssertionError: expected call not found.#x1B[0m
#x1B[1m#x1B[31mE   Expected: create_preprod_artifact(org_id=4557180650323968, project_id=4557180650389504, checksum='6b258c37d65ee20d22942d85c0b660600db4062b', build_configuration_name=None, release_notes=None, head_sha=None, base_sha=None, provider=None, head_repo_name=None, base_repo_name=None, head_ref=None, base_ref=None, pr_number=None)#x1B[0m
#x1B[1m#x1B[31mE     Actual: create_preprod_artifact(org_id=4557180650323968, project_id=4557180650389504, checksum='6b258c37d65ee20d22942d85c0b660600db4062b', build_configuration_name=None, release_notes=None, date_built=None, head_sha=None, base_sha=None, provider=None, head_repo_name=None, base_repo_name=None, head_ref=None, base_ref=None, pr_number=None)#x1B[0m
tests.sentry.preprod.test_tasks.AssemblePreprodArtifactTest::test_create_preprod_artifact_with_date_built
Stack Traces | 8.54s run time
#x1B[1m#x1B[.../sentry/preprod/test_tasks.py#x1B[0m:131: in test_create_preprod_artifact_with_date_built
    assert artifact is not None
#x1B[1m#x1B[31mE   assert None is not None#x1B[0m

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants