Exemplo n.º 1
0
    def post(self, request: Request, project: Project) -> Response:
        serializer = AppStoreCreateCredentialsSerializer(data=request.data)
        if not serializer.is_valid():
            return Response(serializer.errors, status=400)
        config = serializer.validated_data
        session_context = config.pop("sessionContext")
        try:
            itunes_client = itunes_connect.ITunesClient.from_json(
                session_context["client_state"])
        except Exception:
            return Response({"session_context": ["Invalid client_state"]},
                            status=400)

        config["type"] = "appStoreConnect"
        config["id"] = uuid4().hex
        config["name"] = config["appName"]
        config["itunesCreated"] = session_context["itunes_created"]
        config["itunesSession"] = itunes_client.session_cookie()

        # This field is renamed in the backend to represent its actual value, for the UI it
        # is just an opaque value.
        config["orgPublicId"] = config.pop("orgId")

        try:
            validated_config = appconnect.AppStoreConnectConfig.from_json(
                config)
        except ValueError:
            raise AppConnectMultipleSourcesError
        allow_multiple = features.has(MULTIPLE_SOURCES_FEATURE_NAME,
                                      project.organization,
                                      actor=request.user)
        try:
            new_sources = validated_config.update_project_symbol_source(
                project, allow_multiple)
        except ValueError:
            raise AppConnectMultipleSourcesError

        redacted_sources = redact_source_secrets(new_sources)
        self.create_audit_entry(
            request=request,
            organization=project.organization,
            target_object=project.id,
            event=AuditLogEntryEvent.PROJECT_EDIT,
            data={appconnect.SYMBOL_SOURCES_PROP_NAME: redacted_sources},
        )

        dsym_download.apply_async(kwargs={
            "project_id": project.id,
            "config_id": validated_config.id,
        })

        return Response({"id": validated_config.id}, status=200)
Exemplo n.º 2
0
    def post(self, request: Request, project: Project,
             credentials_id: str) -> Response:
        serializer = AppStoreUpdateCredentialsSerializer(data=request.data)
        if not serializer.is_valid():
            return Response(serializer.errors, status=400)
        data = serializer.validated_data

        try:
            symbol_source_config = appconnect.AppStoreConnectConfig.from_project_config(
                project, credentials_id)
        except KeyError:
            return Response(status=404)

        # Any secrets set to None during validation are meant to be no-ops, so remove them to avoid
        # erasing the existing values
        for secret in secret_fields(symbol_source_config.type):
            if secret in data and data[secret] is None:
                del data[secret]

        new_data = symbol_source_config.to_json()
        new_data.update(data)
        symbol_source_config = appconnect.AppStoreConnectConfig.from_json(
            new_data)

        # We are sure we are only updating, no point in actually checking if multiple are allowed.
        new_sources = symbol_source_config.update_project_symbol_source(
            project, allow_multiple=True)

        redacted_sources = redact_source_secrets(new_sources)
        self.create_audit_entry(
            request=request,
            organization=project.organization,
            target_object=project.id,
            event=AuditLogEntryEvent.PROJECT_EDIT,
            data={appconnect.SYMBOL_SOURCES_PROP_NAME: redacted_sources},
        )

        dsym_download.apply_async(kwargs={
            "project_id": project.id,
            "config_id": symbol_source_config.id,
        })

        return Response(symbol_source_config.to_redacted_json(), status=200)
Exemplo n.º 3
0
    def post(self, request: Request, project: Project) -> Response:
        serializer = AppStoreCreateCredentialsSerializer(data=request.data)
        if not serializer.is_valid():
            return Response(serializer.errors, status=400)
        config = serializer.validated_data

        config["type"] = "appStoreConnect"
        config["id"] = uuid4().hex
        config["name"] = config["appName"]

        try:
            validated_config = appconnect.AppStoreConnectConfig.from_json(
                config)
        except ValueError:
            raise AppConnectMultipleSourcesError
        allow_multiple = features.has(MULTIPLE_SOURCES_FEATURE_NAME,
                                      project.organization,
                                      actor=request.user)
        try:
            new_sources = validated_config.update_project_symbol_source(
                project, allow_multiple)
        except ValueError:
            raise AppConnectMultipleSourcesError

        redacted_sources = redact_source_secrets(new_sources)
        self.create_audit_entry(
            request=request,
            organization=project.organization,
            target_object=project.id,
            event=AuditLogEntryEvent.PROJECT_EDIT,
            data={appconnect.SYMBOL_SOURCES_PROP_NAME: redacted_sources},
        )

        dsym_download.apply_async(kwargs={
            "project_id": project.id,
            "config_id": validated_config.id,
        })

        return Response({"id": validated_config.id}, status=200)
Exemplo n.º 4
0
    def put(self, request: Request, project) -> Response:
        """
        Update a Project
        ````````````````

        Update various attributes and configurable settings for the given
        project.  Only supplied values are updated.

        :pparam string organization_slug: the slug of the organization the
                                          project belongs to.
        :pparam string project_slug: the slug of the project to update.
        :param string name: the new name for the project.
        :param string slug: the new slug for the project.
        :param string team: the slug of new team for the project. Note, will be deprecated
                            soon when multiple teams can have access to a project.
        :param string platform: the new platform for the project.
        :param boolean isBookmarked: in case this API call is invoked with a
                                     user context this allows changing of
                                     the bookmark flag.
        :param int digestsMinDelay:
        :param int digestsMaxDelay:
        :auth: required
        """
        has_project_write = (request.auth and request.auth.has_scope("project:write")) or (
            request.access and request.access.has_scope("project:write")
        )

        changed_proj_settings = {}

        if has_project_write:
            serializer_cls = ProjectAdminSerializer
        else:
            serializer_cls = ProjectMemberSerializer

        serializer = serializer_cls(
            data=request.data, partial=True, context={"project": project, "request": request}
        )
        if not serializer.is_valid():
            return Response(serializer.errors, status=400)

        result = serializer.validated_data

        allow_dynamic_sampling = features.has(
            "organizations:filters-and-sampling", project.organization, actor=request.user
        )

        allow_dynamic_sampling_error_rules = features.has(
            "organizations:filters-and-sampling-error-rules",
            project.organization,
            actor=request.user,
        )

        if not allow_dynamic_sampling and result.get("dynamicSampling"):
            # trying to set dynamic sampling with feature disabled
            return Response(
                {"detail": ["You do not have permission to set dynamic sampling."]},
                status=403,
            )

        if not has_project_write:
            # options isn't part of the serializer, but should not be editable by members
            for key in chain(ProjectAdminSerializer().fields.keys(), ["options"]):
                if request.data.get(key) and not result.get(key):
                    return Response(
                        {"detail": ["You do not have permission to perform this action."]},
                        status=403,
                    )

        changed = False

        old_slug = None
        if result.get("slug"):
            old_slug = project.slug
            project.slug = result["slug"]
            changed = True
            changed_proj_settings["new_slug"] = project.slug
            changed_proj_settings["old_slug"] = old_slug

        if result.get("name"):
            project.name = result["name"]
            changed = True
            changed_proj_settings["new_project"] = project.name

        old_team_id = None
        new_team = None
        if result.get("team"):
            return Response(
                {"detail": ["Editing a team via this endpoint has been deprecated."]}, status=400
            )

        if result.get("platform"):
            project.platform = result["platform"]
            changed = True

        if changed:
            project.save()
            if old_team_id is not None:
                ProjectTeam.objects.filter(project=project, team_id=old_team_id).update(
                    team=new_team
                )

            if old_slug:
                ProjectRedirect.record(project, old_slug)

        if result.get("isBookmarked"):
            try:
                with transaction.atomic():
                    ProjectBookmark.objects.create(project_id=project.id, user=request.user)
            except IntegrityError:
                pass
        elif result.get("isBookmarked") is False:
            ProjectBookmark.objects.filter(project_id=project.id, user=request.user).delete()

        if result.get("digestsMinDelay"):
            project.update_option("digests:mail:minimum_delay", result["digestsMinDelay"])
        if result.get("digestsMaxDelay"):
            project.update_option("digests:mail:maximum_delay", result["digestsMaxDelay"])
        if result.get("subjectPrefix") is not None:
            if project.update_option("mail:subject_prefix", result["subjectPrefix"]):
                changed_proj_settings["mail:subject_prefix"] = result["subjectPrefix"]
        if result.get("subjectTemplate"):
            project.update_option("mail:subject_template", result["subjectTemplate"])
        if result.get("scrubIPAddresses") is not None:
            if project.update_option("sentry:scrub_ip_address", result["scrubIPAddresses"]):
                changed_proj_settings["sentry:scrub_ip_address"] = result["scrubIPAddresses"]
        if result.get("groupingConfig") is not None:
            if project.update_option("sentry:grouping_config", result["groupingConfig"]):
                changed_proj_settings["sentry:grouping_config"] = result["groupingConfig"]
        if result.get("groupingEnhancements") is not None:
            if project.update_option(
                "sentry:grouping_enhancements", result["groupingEnhancements"]
            ):
                changed_proj_settings["sentry:grouping_enhancements"] = result[
                    "groupingEnhancements"
                ]
        if result.get("fingerprintingRules") is not None:
            if project.update_option("sentry:fingerprinting_rules", result["fingerprintingRules"]):
                changed_proj_settings["sentry:fingerprinting_rules"] = result["fingerprintingRules"]
        if result.get("secondaryGroupingConfig") is not None:
            if project.update_option(
                "sentry:secondary_grouping_config", result["secondaryGroupingConfig"]
            ):
                changed_proj_settings["sentry:secondary_grouping_config"] = result[
                    "secondaryGroupingConfig"
                ]

        if result.get("secondaryGroupingExpiry") is not None:
            if project.update_option(
                "sentry:secondary_grouping_expiry", result["secondaryGroupingExpiry"]
            ):
                changed_proj_settings["sentry:secondary_grouping_expiry"] = result[
                    "secondaryGroupingExpiry"
                ]
        if result.get("securityToken") is not None:
            if project.update_option("sentry:token", result["securityToken"]):
                changed_proj_settings["sentry:token"] = result["securityToken"]
        if result.get("securityTokenHeader") is not None:
            if project.update_option("sentry:token_header", result["securityTokenHeader"]):
                changed_proj_settings["sentry:token_header"] = result["securityTokenHeader"]
        if result.get("verifySSL") is not None:
            if project.update_option("sentry:verify_ssl", result["verifySSL"]):
                changed_proj_settings["sentry:verify_ssl"] = result["verifySSL"]
        if result.get("dataScrubber") is not None:
            if project.update_option("sentry:scrub_data", result["dataScrubber"]):
                changed_proj_settings["sentry:scrub_data"] = result["dataScrubber"]
        if result.get("dataScrubberDefaults") is not None:
            if project.update_option("sentry:scrub_defaults", result["dataScrubberDefaults"]):
                changed_proj_settings["sentry:scrub_defaults"] = result["dataScrubberDefaults"]
        if result.get("sensitiveFields") is not None:
            if project.update_option("sentry:sensitive_fields", result["sensitiveFields"]):
                changed_proj_settings["sentry:sensitive_fields"] = result["sensitiveFields"]
        if result.get("safeFields") is not None:
            if project.update_option("sentry:safe_fields", result["safeFields"]):
                changed_proj_settings["sentry:safe_fields"] = result["safeFields"]
        if "storeCrashReports" in result is not None:
            if project.get_option("sentry:store_crash_reports") != result["storeCrashReports"]:
                changed_proj_settings["sentry:store_crash_reports"] = result["storeCrashReports"]
                if result["storeCrashReports"] is None:
                    project.delete_option("sentry:store_crash_reports")
                else:
                    project.update_option("sentry:store_crash_reports", result["storeCrashReports"])
        if result.get("relayPiiConfig") is not None:
            if project.update_option("sentry:relay_pii_config", result["relayPiiConfig"]):
                changed_proj_settings["sentry:relay_pii_config"] = (
                    result["relayPiiConfig"].strip() or None
                )
        if result.get("builtinSymbolSources") is not None:
            if project.update_option(
                "sentry:builtin_symbol_sources", result["builtinSymbolSources"]
            ):
                changed_proj_settings["sentry:builtin_symbol_sources"] = result[
                    "builtinSymbolSources"
                ]
        if result.get("symbolSources") is not None:
            if project.update_option("sentry:symbol_sources", result["symbolSources"]):
                # Redact secrets so they don't get logged directly to the Audit Log
                sources_json = result["symbolSources"] or None
                try:
                    sources = parse_sources(sources_json)
                except Exception:
                    sources = []
                redacted_sources = redact_source_secrets(sources)
                changed_proj_settings["sentry:symbol_sources"] = redacted_sources
        if "defaultEnvironment" in result:
            if result["defaultEnvironment"] is None:
                project.delete_option("sentry:default_environment")
            else:
                project.update_option("sentry:default_environment", result["defaultEnvironment"])
        # resolveAge can be None
        if "resolveAge" in result:
            if project.update_option(
                "sentry:resolve_age",
                0 if result.get("resolveAge") is None else int(result["resolveAge"]),
            ):
                changed_proj_settings["sentry:resolve_age"] = result["resolveAge"]
        if result.get("scrapeJavaScript") is not None:
            if project.update_option("sentry:scrape_javascript", result["scrapeJavaScript"]):
                changed_proj_settings["sentry:scrape_javascript"] = result["scrapeJavaScript"]
        if result.get("allowedDomains"):
            if project.update_option("sentry:origins", result["allowedDomains"]):
                changed_proj_settings["sentry:origins"] = result["allowedDomains"]

        if "isSubscribed" in result:
            NotificationSetting.objects.update_settings(
                ExternalProviders.EMAIL,
                NotificationSettingTypes.ISSUE_ALERTS,
                get_option_value_from_boolean(result.get("isSubscribed")),
                user=request.user,
                project=project,
            )

        if "dynamicSampling" in result:
            raw_dynamic_sampling = result["dynamicSampling"]
            if (
                not allow_dynamic_sampling_error_rules
                and self._dynamic_sampling_contains_error_rule(raw_dynamic_sampling)
            ):
                return Response(
                    {
                        "detail": [
                            "Dynamic Sampling only accepts rules of type transaction or trace"
                        ]
                    },
                    status=400,
                )

            fixed_rules = self._fix_rule_ids(project, raw_dynamic_sampling)
            project.update_option("sentry:dynamic_sampling", fixed_rules)

        # TODO(dcramer): rewrite options to use standard API config
        if has_project_write:
            options = request.data.get("options", {})
            if "sentry:origins" in options:
                project.update_option(
                    "sentry:origins", clean_newline_inputs(options["sentry:origins"])
                )
            if "sentry:resolve_age" in options:
                project.update_option("sentry:resolve_age", int(options["sentry:resolve_age"]))
            if "sentry:scrub_data" in options:
                project.update_option("sentry:scrub_data", bool(options["sentry:scrub_data"]))
            if "sentry:scrub_defaults" in options:
                project.update_option(
                    "sentry:scrub_defaults", bool(options["sentry:scrub_defaults"])
                )
            if "sentry:safe_fields" in options:
                project.update_option(
                    "sentry:safe_fields", [s.strip().lower() for s in options["sentry:safe_fields"]]
                )
            if "sentry:store_crash_reports" in options:
                project.update_option(
                    "sentry:store_crash_reports",
                    convert_crashreport_count(
                        options["sentry:store_crash_reports"], allow_none=True
                    ),
                )
            if "sentry:relay_pii_config" in options:
                project.update_option(
                    "sentry:relay_pii_config", options["sentry:relay_pii_config"].strip() or None
                )
            if "sentry:sensitive_fields" in options:
                project.update_option(
                    "sentry:sensitive_fields",
                    [s.strip().lower() for s in options["sentry:sensitive_fields"]],
                )
            if "sentry:scrub_ip_address" in options:
                project.update_option(
                    "sentry:scrub_ip_address", bool(options["sentry:scrub_ip_address"])
                )
            if "sentry:grouping_config" in options:
                project.update_option("sentry:grouping_config", options["sentry:grouping_config"])
            if "sentry:fingerprinting_rules" in options:
                project.update_option(
                    "sentry:fingerprinting_rules", options["sentry:fingerprinting_rules"]
                )
            if "mail:subject_prefix" in options:
                project.update_option("mail:subject_prefix", options["mail:subject_prefix"])
            if "sentry:default_environment" in options:
                project.update_option(
                    "sentry:default_environment", options["sentry:default_environment"]
                )
            if "sentry:csp_ignored_sources_defaults" in options:
                project.update_option(
                    "sentry:csp_ignored_sources_defaults",
                    bool(options["sentry:csp_ignored_sources_defaults"]),
                )
            if "sentry:csp_ignored_sources" in options:
                project.update_option(
                    "sentry:csp_ignored_sources",
                    clean_newline_inputs(options["sentry:csp_ignored_sources"]),
                )
            if "sentry:blacklisted_ips" in options:
                project.update_option(
                    "sentry:blacklisted_ips",
                    clean_newline_inputs(options["sentry:blacklisted_ips"]),
                )
            if "feedback:branding" in options:
                project.update_option(
                    "feedback:branding", "1" if options["feedback:branding"] else "0"
                )
            if "sentry:reprocessing_active" in options:
                project.update_option(
                    "sentry:reprocessing_active", bool(options["sentry:reprocessing_active"])
                )
            if "filters:blacklisted_ips" in options:
                project.update_option(
                    "sentry:blacklisted_ips",
                    clean_newline_inputs(options["filters:blacklisted_ips"]),
                )
            if f"filters:{FilterTypes.RELEASES}" in options:
                if features.has("projects:custom-inbound-filters", project, actor=request.user):
                    project.update_option(
                        f"sentry:{FilterTypes.RELEASES}",
                        clean_newline_inputs(options[f"filters:{FilterTypes.RELEASES}"]),
                    )
                else:
                    return Response(
                        {"detail": ["You do not have that feature enabled"]}, status=400
                    )
            if f"filters:{FilterTypes.ERROR_MESSAGES}" in options:
                if features.has("projects:custom-inbound-filters", project, actor=request.user):
                    project.update_option(
                        f"sentry:{FilterTypes.ERROR_MESSAGES}",
                        clean_newline_inputs(
                            options[f"filters:{FilterTypes.ERROR_MESSAGES}"],
                            case_insensitive=False,
                        ),
                    )
                else:
                    return Response(
                        {"detail": ["You do not have that feature enabled"]}, status=400
                    )
            if "copy_from_project" in result:
                if not project.copy_settings_from(result["copy_from_project"]):
                    return Response({"detail": ["Copy project settings failed."]}, status=409)

            self.create_audit_entry(
                request=request,
                organization=project.organization,
                target_object=project.id,
                event=AuditLogEntryEvent.PROJECT_EDIT,
                data=changed_proj_settings,
            )

        data = serialize(project, request.user, DetailedProjectSerializer())
        return Response(data)
Exemplo n.º 5
0
    def post(self, request: Request, project: Project,
             credentials_id: str) -> Response:
        serializer = AppStoreUpdateCredentialsSerializer(data=request.data)
        if not serializer.is_valid():
            return Response(serializer.errors, status=400)
        data = serializer.validated_data
        session_context = data.pop("sessionContext")
        try:
            itunes_client = itunes_connect.ITunesClient.from_json(
                session_context["client_state"])
        except Exception:
            return Response({"session_context": ["Invalid client_state"]},
                            status=400)

        # get the existing credentials
        try:
            symbol_source_config = appconnect.AppStoreConnectConfig.from_project_config(
                project, credentials_id)
        except KeyError:
            return Response(status=404)

        # get the new credentials
        if session_context:
            data["itunesCreated"] = session_context.get("itunes_created")
            data["itunesSession"] = itunes_client.session_cookie()

        if "orgId" in data:
            # This field is renamed in the backend to represent its actual value, for the UI
            # it is just an opaque value.
            data["orgPublicId"] = data.pop("orgId")

        # Any secrets set to None during validation are meant to be no-ops, so remove them to avoid
        # erasing the existing values
        for secret in secret_fields(symbol_source_config.type):
            if secret in data and data[secret] is None:
                del data[secret]

        new_data = symbol_source_config.to_json()
        new_data.update(data)
        symbol_source_config = appconnect.AppStoreConnectConfig.from_json(
            new_data)

        # We are sure we are only updating, no point in actually checking if multiple are allowed.
        new_sources = symbol_source_config.update_project_symbol_source(
            project, allow_multiple=True)

        redacted_sources = redact_source_secrets(new_sources)
        self.create_audit_entry(
            request=request,
            organization=project.organization,
            target_object=project.id,
            event=AuditLogEntryEvent.PROJECT_EDIT,
            data={appconnect.SYMBOL_SOURCES_PROP_NAME: redacted_sources},
        )

        dsym_download.apply_async(kwargs={
            "project_id": project.id,
            "config_id": symbol_source_config.id,
        })

        return Response(symbol_source_config.to_redacted_json(), status=200)
Exemplo n.º 6
0
    def serialize(self, obj, attrs, user):
        from sentry.plugins.base import plugins

        def get_value_with_default(key):
            value = attrs["options"].get(key)
            if value is not None:
                return value
            return projectoptions.get_well_known_default(
                key, epoch=attrs["options"].get("sentry:option-epoch")
            )

        data = super().serialize(obj, attrs, user)
        data.update(
            {
                "latestRelease": attrs["latest_release"],
                "options": {
                    "sentry:csp_ignored_sources_defaults": bool(
                        attrs["options"].get("sentry:csp_ignored_sources_defaults", True)
                    ),
                    "sentry:csp_ignored_sources": "\n".join(
                        attrs["options"].get("sentry:csp_ignored_sources", []) or []
                    ),
                    "sentry:reprocessing_active": bool(
                        attrs["options"].get("sentry:reprocessing_active", False)
                    ),
                    "filters:blacklisted_ips": "\n".join(
                        attrs["options"].get("sentry:blacklisted_ips", [])
                    ),
                    f"filters:{FilterTypes.RELEASES}": "\n".join(
                        attrs["options"].get(f"sentry:{FilterTypes.RELEASES}", [])
                    ),
                    f"filters:{FilterTypes.ERROR_MESSAGES}": "\n".join(
                        attrs["options"].get(f"sentry:{FilterTypes.ERROR_MESSAGES}", [])
                    ),
                    "feedback:branding": attrs["options"].get("feedback:branding", "1") == "1",
                },
                "digestsMinDelay": attrs["options"].get(
                    "digests:mail:minimum_delay", digests.minimum_delay
                ),
                "digestsMaxDelay": attrs["options"].get(
                    "digests:mail:maximum_delay", digests.maximum_delay
                ),
                "subjectPrefix": attrs["options"].get(
                    "mail:subject_prefix", options.get("mail.subject-prefix")
                ),
                "allowedDomains": attrs["options"].get("sentry:origins", ["*"]),
                "resolveAge": int(attrs["options"].get("sentry:resolve_age", 0)),
                "dataScrubber": bool(attrs["options"].get("sentry:scrub_data", True)),
                "dataScrubberDefaults": bool(attrs["options"].get("sentry:scrub_defaults", True)),
                "safeFields": attrs["options"].get("sentry:safe_fields", []),
                "storeCrashReports": convert_crashreport_count(
                    attrs["options"].get("sentry:store_crash_reports"), allow_none=True
                ),
                "sensitiveFields": attrs["options"].get("sentry:sensitive_fields", []),
                "subjectTemplate": attrs["options"].get("mail:subject_template")
                or DEFAULT_SUBJECT_TEMPLATE.template,
                "securityToken": attrs["options"].get("sentry:token") or obj.get_security_token(),
                "securityTokenHeader": attrs["options"].get("sentry:token_header"),
                "verifySSL": bool(attrs["options"].get("sentry:verify_ssl", False)),
                "scrubIPAddresses": bool(attrs["options"].get("sentry:scrub_ip_address", False)),
                "scrapeJavaScript": bool(attrs["options"].get("sentry:scrape_javascript", True)),
                "groupingConfig": get_value_with_default("sentry:grouping_config"),
                "groupingEnhancements": get_value_with_default("sentry:grouping_enhancements"),
                "groupingEnhancementsBase": get_value_with_default(
                    "sentry:grouping_enhancements_base"
                ),
                "secondaryGroupingExpiry": get_value_with_default(
                    "sentry:secondary_grouping_expiry"
                ),
                "secondaryGroupingConfig": get_value_with_default(
                    "sentry:secondary_grouping_config"
                ),
                "fingerprintingRules": get_value_with_default("sentry:fingerprinting_rules"),
                "organization": attrs["org"],
                "plugins": serialize(
                    [
                        plugin
                        for plugin in plugins.configurable_for_project(obj, version=None)
                        if plugin.has_project_conf()
                    ],
                    user,
                    PluginSerializer(obj),
                ),
                "platforms": attrs["platforms"],
                "processingIssues": attrs["processing_issues"],
                "defaultEnvironment": attrs["options"].get("sentry:default_environment"),
                "relayPiiConfig": attrs["options"].get("sentry:relay_pii_config"),
                "builtinSymbolSources": get_value_with_default("sentry:builtin_symbol_sources"),
                "dynamicSampling": get_value_with_default("sentry:dynamic_sampling"),
                "eventProcessing": {
                    "symbolicationDegraded": False,
                },
            }
        )
        custom_symbol_sources_json = attrs["options"].get("sentry:symbol_sources")
        try:
            sources = parse_sources(custom_symbol_sources_json, False)
        except Exception:
            # In theory sources stored on the project should be valid. If they are invalid, we don't
            # want to abort serialization just for sources, so just return an empty list instead of
            # returning sources with their secrets included.
            serialized_sources = "[]"
        else:
            redacted_sources = redact_source_secrets(sources)
            serialized_sources = json.dumps(redacted_sources)

        data.update(
            {
                "symbolSources": serialized_sources,
            }
        )

        return data